diff --git a/.ci/Debian13/Dockerfile b/.ci/Debian13/Dockerfile new file mode 100644 index 000000000..d7ab6ac86 --- /dev/null +++ b/.ci/Debian13/Dockerfile @@ -0,0 +1,29 @@ +FROM debian:13 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Fedora41/Dockerfile b/.ci/Fedora43/Dockerfile similarity index 95% rename from .ci/Fedora41/Dockerfile rename to .ci/Fedora43/Dockerfile index fc9e86c2e..27570cf99 100644 --- a/.ci/Fedora41/Dockerfile +++ b/.ci/Fedora43/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:41 +FROM fedora:43 RUN dnf install -y \ ccache \ diff --git a/.ci/Servatrice_Debian11/Dockerfile b/.ci/Servatrice_Debian11/Dockerfile new file mode 100644 index 000000000..fadc9e0e7 --- /dev/null +++ b/.ci/Servatrice_Debian11/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:11 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt5sql5-mysql \ + libqt5websockets5-dev \ + ninja-build \ + protobuf-compiler \ + qttools5-dev \ + qttools5-dev-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu22.04/Dockerfile b/.ci/Ubuntu22.04/Dockerfile index ff2e6e43b..93c8fdea9 100644 --- a/.ci/Ubuntu22.04/Dockerfile +++ b/.ci/Ubuntu22.04/Dockerfile @@ -9,20 +9,18 @@ RUN apt-get update && \ file \ g++ \ git \ - libgl-dev \ liblzma-dev \ libmariadb-dev-compat \ libprotobuf-dev \ - libqt6multimedia6 \ - libqt6sql6-mysql \ - libqt6svg6-dev \ - libqt6websockets6-dev \ + libqt5multimedia5-plugins \ + libqt5sql5-mysql \ + libqt5svg5-dev \ + libqt5websockets5-dev \ ninja-build \ protobuf-compiler \ - qt6-image-formats-plugins \ - qt6-l10n-tools \ - qt6-multimedia-dev \ - qt6-tools-dev \ - qt6-tools-dev-tools \ + qt5-image-formats-plugins \ + qtmultimedia5-dev \ + qttools5-dev \ + qttools5-dev-tools \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/.ci/Ubuntu26.04/Dockerfile b/.ci/Ubuntu26.04/Dockerfile new file mode 100644 index 000000000..7b0cd389f --- /dev/null +++ b/.ci/Ubuntu26.04/Dockerfile @@ -0,0 +1,29 @@ +FROM ubuntu:26.04 + +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + clang-format \ + cmake \ + file \ + g++ \ + git \ + libgl-dev \ + liblzma-dev \ + libmariadb-dev-compat \ + libprotobuf-dev \ + libqt6multimedia6 \ + libqt6sql6-mysql \ + ninja-build \ + protobuf-compiler \ + qt6-image-formats-plugins \ + qt6-l10n-tools \ + qt6-multimedia-dev \ + qt6-svg-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ + qt6-websockets-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/compile.sh b/.ci/compile.sh index ffe733c72..7ebdd6e4e 100755 --- a/.ci/compile.sh +++ b/.ci/compile.sh @@ -10,8 +10,11 @@ # --test runs tests # --debug or --release sets the build type ie CMAKE_BUILD_TYPE # --ccache [] uses ccache and shows stats, optionally provide size +# --evict-ccache runs ccache eviction based on given age after build # --dir sets the name of the build dir, default is "build" -# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_TEST USE_CCACHE CCACHE_SIZE BUILD_DIR CMAKE_GENERATOR +# --cmake-generator sets CMAKE_GENERATOR as used by cmake +# --target-macos-version sets the min os version - only used for macOS builds +# uses env: BUILDTYPE MAKE_INSTALL MAKE_PACKAGE PACKAGE_TYPE PACKAGE_SUFFIX MAKE_SERVER MAKE_NO_CLIENT MAKE_TEST USE_CCACHE CCACHE_SIZE CCACHE_EVICTION_AGE BUILD_DIR CMAKE_GENERATOR TARGET_MACOS_VERSION # (correspond to args: --debug/--release --install --package --suffix --server --test --ccache --dir ) # exitcode: 1 for failure, 3 for invalid arguments @@ -46,6 +49,10 @@ while [[ $# != 0 ]]; do MAKE_SERVER=1 shift ;; + '--no-client') + MAKE_NO_CLIENT=1 + shift + ;; '--test') MAKE_TEST=1 shift @@ -66,6 +73,19 @@ while [[ $# != 0 ]]; do shift fi ;; + '--evict-ccache') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--evict-ccache expects an argument" + exit 3 + fi + CCACHE_EVICTION_AGE=$1 + shift + ;; + '--vcpkg') + USE_VCPKG=1 + shift + ;; '--dir') shift if [[ $# == 0 ]]; then @@ -75,6 +95,24 @@ while [[ $# != 0 ]]; do BUILD_DIR="$1" shift ;; + '--cmake-generator') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--cmake-generator expects an argument" + exit 3 + fi + export CMAKE_GENERATOR=$1 + shift + ;; + '--target-macos-version') + shift + if [[ $# == 0 ]]; then + echo "::error file=$0::--target-macos-version expects an argument" + exit 3 + fi + TARGET_MACOS_VERSION="$1" + shift + ;; *) echo "::error file=$0::unrecognized option: $1" exit 3 @@ -95,11 +133,17 @@ fi mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" +# Set minimum CMake Version +export CMAKE_POLICY_VERSION_MINIMUM=3.10 + # Add cmake flags flags=("-DCMAKE_BUILD_TYPE=$BUILDTYPE") if [[ $MAKE_SERVER ]]; then flags+=("-DWITH_SERVER=1") fi +if [[ $MAKE_NO_CLIENT ]]; then + flags+=("-DWITH_CLIENT=0" "-DWITH_ORACLE=0") +fi if [[ $MAKE_TEST ]]; then flags+=("-DTEST=1") fi @@ -113,6 +157,9 @@ fi if [[ $PACKAGE_TYPE ]]; then flags+=("-DCPACK_GENERATOR=$PACKAGE_TYPE") fi +if [[ $USE_VCPKG ]]; then + flags+=("-DUSE_VCPKG=1") +fi # Add cmake --build flags buildflags=(--config "$BUILDTYPE") @@ -129,9 +176,47 @@ function ccachestatsverbose() { # Compile if [[ $RUNNER_OS == macOS ]]; then + # QTDIR is needed for macOS since we actually only use the cached thin Qt binaries instead of the install-qt-action, + # which sets a few environment variables + if QTDIR=$(find "$GITHUB_WORKSPACE/Qt" -depth -maxdepth 2 -name macos -type d -print -quit); then + echo "found QTDIR at $QTDIR" + else + echo "could not find QTDIR!" + exit 2 + fi + # the qtdir is located at Qt/[qtversion]/macos + # we use find to get the first subfolder with the name "macos" + # this works independent of the qt version as there should be only one version installed on the runner at a time + export QTDIR + + if [[ $TARGET_MACOS_VERSION ]]; then + # CMAKE_OSX_DEPLOYMENT_TARGET is a vanilla cmake flag needed to compile to target macOS version + flags+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=$TARGET_MACOS_VERSION") + + # vcpkg dependencies need a vcpkg triplet file to compile to the target macOS version + # an easy way is to copy the x64-osx.cmake file and modify it + triplets_dir="/tmp/cmake/triplets" + triplet_version="custom-triplet" + triplet_file="$triplets_dir/$triplet_version.cmake" + arch=$(uname -m) + if [[ $arch == x86_64 ]]; then + arch="x64" + fi + mkdir -p "$triplets_dir" + cp "../vcpkg/triplets/$arch-osx.cmake" "$triplet_file" + echo "set(VCPKG_CMAKE_SYSTEM_VERSION $TARGET_MACOS_VERSION)" >>"$triplet_file" + echo "set(VCPKG_OSX_DEPLOYMENT_TARGET $TARGET_MACOS_VERSION)" >>"$triplet_file" + flags+=("-DVCPKG_OVERLAY_TRIPLETS=$triplets_dir") + flags+=("-DVCPKG_HOST_TRIPLET=$triplet_version") + flags+=("-DVCPKG_TARGET_TRIPLET=$triplet_version") + echo "::group::Generated triplet $triplet_file" + cat "$triplet_file" + echo "::endgroup::" + fi + echo "::group::Signing Certificate" if [[ -n "$MACOS_CERTIFICATE_NAME" ]]; then - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 + echo "$MACOS_CERTIFICATE" | base64 --decode >"certificate.p12" security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain security default-keychain -s build.keychain security set-keychain-settings -t 3600 -l build.keychain @@ -143,6 +228,29 @@ if [[ $RUNNER_OS == macOS ]]; then echo "No signing certificate configured. Skipping set up of keychain in macOS environment." fi echo "::endgroup::" + + if [[ $MAKE_PACKAGE ]]; then + # Workaround https://github.com/actions/runner-images/issues/7522 + # have hdiutil repeat the command 10 times in hope of success + hdiutil_script="/tmp/hdiutil.sh" + # shellcheck disable=SC2016 + echo '#!/bin/bash + i=0 + while ! hdiutil "$@"; do + if (( ++i >= 10 )); then + echo "Error: hdiutil failed $i times!" >&2 + break + fi + sleep 1 + done' >"$hdiutil_script" + chmod +x "$hdiutil_script" + flags+=(-DCPACK_COMMAND_HDIUTIL="$hdiutil_script") + fi + +elif [[ $RUNNER_OS == Windows ]]; then + # Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ + # and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt + buildflags+=(-- -p:UseMultiToolTask=true -p:EnableClServerMode=true) fi if [[ $USE_CCACHE ]]; then @@ -153,23 +261,39 @@ fi echo "::group::Configure cmake" cmake --version +echo "Running cmake with flags: ${flags[*]}" cmake .. "${flags[@]}" echo "::endgroup::" echo "::group::Build project" -if [[ $RUNNER_OS == Windows ]]; then - # Enable MTT, see https://devblogs.microsoft.com/cppblog/improved-parallelism-in-msbuild/ - # and https://devblogs.microsoft.com/cppblog/cpp-build-throughput-investigation-and-tune-up/#multitooltask-mtt - cmake --build . "${buildflags[@]}" -- -p:UseMultiToolTask=true -p:EnableClServerMode=true -else - cmake --build . "${buildflags[@]}" -fi +echo "Running cmake --build with flags: ${buildflags[*]}" +cmake --build . "${buildflags[@]}" echo "::endgroup::" if [[ $USE_CCACHE ]]; then + if [[ $CCACHE_EVICTION_AGE ]]; then + echo "::group::evict ccache files older than $CCACHE_EVICTION_AGE" + ccache --evict-older-than "$CCACHE_EVICTION_AGE" + echo "::endgroup::" + fi echo "::group::Show ccache stats again" ccachestatsverbose echo "::endgroup::" +elif [[ $CCACHE_EVICTION_AGE ]]; then + echo "::error file=$0::ccache eviction is enabled while ccache is disabled!" +fi + +if [[ $RUNNER_OS == macOS ]]; then + echo "::group::Inspect Mach-O binaries" + for app in cockatrice oracle servatrice; do + binary="$GITHUB_WORKSPACE/build/$app/$app.app/Contents/MacOS/$app" + echo "Inspecting $app..." + vtool -show-build "$binary" + file "$binary" + lipo -info "$binary" + echo "" + done + echo "::endgroup::" fi if [[ $MAKE_TEST ]]; then @@ -186,12 +310,6 @@ fi if [[ $MAKE_PACKAGE ]]; then echo "::group::Create package" - - if [[ $RUNNER_OS == macOS ]]; then - # Workaround https://github.com/actions/runner-images/issues/7522 - echo "killing XProtectBehaviorService"; sudo pkill -9 XProtect >/dev/null || true; - echo "waiting for XProtectBehaviorService kill"; while pgrep "XProtect"; do sleep 3; done; - fi cmake --build . --target package --config "$BUILDTYPE" echo "::endgroup::" diff --git a/.ci/docker.sh b/.ci/docker.sh index a9fcfcc5b..46112daaa 100644 --- a/.ci/docker.sh +++ b/.ci/docker.sh @@ -3,17 +3,28 @@ # 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. -# sets the name of the docker image, these correspond to directories in .ci +# +# usage: source + + + + + + + + + + diff --git a/doc/doxygen/html/header.html b/doc/doxygen/html/header.html new file mode 100644 index 000000000..261581add --- /dev/null +++ b/doc/doxygen/html/header.html @@ -0,0 +1,81 @@ + + + + + + + + + $projectname: $title + $title + + + + + + + + + + + + + $treeview + $search + $mathjax + $darkmode + + $extrastylesheet + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber + +
+ +
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/doc/doxygen/images/classic_database_display.png b/doc/doxygen/images/classic_database_display.png new file mode 100644 index 000000000..fb762d9a3 Binary files /dev/null and b/doc/doxygen/images/classic_database_display.png differ diff --git a/doc/doxygen/images/classic_database_display_add_buttons.png b/doc/doxygen/images/classic_database_display_add_buttons.png new file mode 100644 index 000000000..b9cb7cd32 Binary files /dev/null and b/doc/doxygen/images/classic_database_display_add_buttons.png differ diff --git a/doc/doxygen/images/classic_deck_editor.png b/doc/doxygen/images/classic_deck_editor.png new file mode 100644 index 000000000..9371651e9 Binary files /dev/null and b/doc/doxygen/images/classic_deck_editor.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list.png b/doc/doxygen/images/deck_dock_deck_list.png new file mode 100644 index 000000000..bf12d97c2 Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list_buttons.png b/doc/doxygen/images/deck_dock_deck_list_buttons.png new file mode 100644 index 000000000..cb66b41da Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list_buttons.png differ diff --git a/doc/doxygen/images/deck_dock_deck_list_group_by.png b/doc/doxygen/images/deck_dock_deck_list_group_by.png new file mode 100644 index 000000000..ba23b1fb5 Binary files /dev/null and b/doc/doxygen/images/deck_dock_deck_list_group_by.png differ diff --git a/doc/doxygen/images/deckeditordeckdockwidget.png b/doc/doxygen/images/deckeditordeckdockwidget.png new file mode 100644 index 000000000..33b9588d0 Binary files /dev/null and b/doc/doxygen/images/deckeditordeckdockwidget.png differ diff --git a/doc/doxygen/images/printing_selector.png b/doc/doxygen/images/printing_selector.png new file mode 100644 index 000000000..c06ac90bc Binary files /dev/null and b/doc/doxygen/images/printing_selector.png differ diff --git a/doc/doxygen/images/printing_selector_disable.png b/doc/doxygen/images/printing_selector_disable.png new file mode 100644 index 000000000..8bdd8f292 Binary files /dev/null and b/doc/doxygen/images/printing_selector_disable.png differ diff --git a/doc/doxygen/images/printing_selector_enable.png b/doc/doxygen/images/printing_selector_enable.png new file mode 100644 index 000000000..2c7d05afa Binary files /dev/null and b/doc/doxygen/images/printing_selector_enable.png differ diff --git a/doc/doxygen/images/printing_selector_navigation.png b/doc/doxygen/images/printing_selector_navigation.png new file mode 100644 index 000000000..a91ed4511 Binary files /dev/null and b/doc/doxygen/images/printing_selector_navigation.png differ diff --git a/doc/doxygen/images/printing_selector_options.png b/doc/doxygen/images/printing_selector_options.png new file mode 100644 index 000000000..2079b70ac Binary files /dev/null and b/doc/doxygen/images/printing_selector_options.png differ diff --git a/doc/doxygen/images/printing_selector_pre_providerid.png b/doc/doxygen/images/printing_selector_pre_providerid.png new file mode 100644 index 000000000..a1be993ee Binary files /dev/null and b/doc/doxygen/images/printing_selector_pre_providerid.png differ diff --git a/doc/doxygen/images/release_channel_beta.png b/doc/doxygen/images/release_channel_beta.png new file mode 100644 index 000000000..00869a9cf Binary files /dev/null and b/doc/doxygen/images/release_channel_beta.png differ diff --git a/doc/doxygen/images/vde_deck_analytics.png b/doc/doxygen/images/vde_deck_analytics.png new file mode 100644 index 000000000..0b4b11e19 Binary files /dev/null and b/doc/doxygen/images/vde_deck_analytics.png differ diff --git a/doc/doxygen/images/vde_flat_layout_color_grouped.png b/doc/doxygen/images/vde_flat_layout_color_grouped.png new file mode 100644 index 000000000..51f176668 Binary files /dev/null and b/doc/doxygen/images/vde_flat_layout_color_grouped.png differ diff --git a/doc/doxygen/images/vde_flat_layout_type_grouped.png b/doc/doxygen/images/vde_flat_layout_type_grouped.png new file mode 100644 index 000000000..4a33e0017 Binary files /dev/null and b/doc/doxygen/images/vde_flat_layout_type_grouped.png differ diff --git a/doc/doxygen/images/vde_overlap_layout_type_grouped.png b/doc/doxygen/images/vde_overlap_layout_type_grouped.png new file mode 100644 index 000000000..606144586 Binary files /dev/null and b/doc/doxygen/images/vde_overlap_layout_type_grouped.png differ diff --git a/doc/doxygen/images/vde_sample_hand.png b/doc/doxygen/images/vde_sample_hand.png new file mode 100644 index 000000000..95924a5d4 Binary files /dev/null and b/doc/doxygen/images/vde_sample_hand.png differ diff --git a/doc/doxygen/js/graph_toggle.js b/doc/doxygen/js/graph_toggle.js new file mode 100644 index 000000000..3d8fa83d0 --- /dev/null +++ b/doc/doxygen/js/graph_toggle.js @@ -0,0 +1,147 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll(".memdoc").forEach(memdoc => { + let callContent = null, callHeader = null; + let callerContent = null, callerHeader = null; + + memdoc.querySelectorAll("div.dynheader").forEach(header => { + const text = (header.textContent || "").trim().toLowerCase(); + let content = header.nextElementSibling; + let tries = 0; + while (content && !content.classList.contains("dyncontent") && tries < 8) { + content = content.nextElementSibling; + tries++; + } + if (!content) return; + + if (text.includes("caller")) { + callerContent = content; + callerHeader = header; + } else if (text.includes("call graph") || text.includes("callgraph") || text.includes("call graph for")) { + callContent = content; + callHeader = header; + } else if (text.includes("call")) { + if (!callContent) { + callContent = content; + callHeader = header; + } + } + }); + + if (!callContent && !callerContent) return; + if (memdoc.querySelector(".graph-toggle")) return; + + const toggle = document.createElement("div"); + toggle.className = "graph-toggle"; + + const callerBtn = document.createElement("button"); + callerBtn.type = "button"; + callerBtn.textContent = "Caller Graph"; + const callBtn = document.createElement("button"); + callBtn.type = "button"; + callBtn.textContent = "Call Graph"; + + toggle.appendChild(callerBtn); + toggle.appendChild(callBtn); + + const firstHeader = memdoc.querySelector("div.dynheader"); + memdoc.insertBefore(toggle, firstHeader || memdoc.firstChild); + + // hide everything initially + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + + // disable missing buttons + if (!callerContent) { + callerBtn.disabled = true; + callerBtn.classList.add("disabled"); + } + if (!callContent) { + callBtn.disabled = true; + callBtn.classList.add("disabled"); + } + + // track current state + let current = null; // "caller", "call", "both", null=hidden + + function setActive(type) { + if (type === "caller") { + if (current === "caller") { // hide it + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = null; + } else if (current === "call") { // show both + if (callerContent) { + callerContent.style.display = "block"; + callerHeader.style.display = "block"; + } + current = "both"; + } else if (current === "both") { // hide caller only → call only + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = "call"; + } else { // nothing visible → show caller + if (callerContent) { + callerContent.style.display = "block"; + callerHeader.style.display = "block"; + } + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = "caller"; + } + } else if (type === "call") { + if (current === "call") { // hide it + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = null; + } else if (current === "caller") { // show both + if (callContent) { + callContent.style.display = "block"; + callHeader.style.display = "block"; + } + current = "both"; + } else if (current === "both") { // hide call only → caller only + if (callContent) { + callContent.style.display = "none"; + callHeader.style.display = "none"; + } + current = "caller"; + } else { // nothing visible → show call only + if (callContent) { + callContent.style.display = "block"; + callHeader.style.display = "block"; + } + if (callerContent) { + callerContent.style.display = "none"; + callerHeader.style.display = "none"; + } + current = "call"; + } + } + + // update button styles + callerBtn.classList.toggle("active", current === "caller" || current === "both"); + callBtn.classList.toggle("active", current === "call" || current === "both"); + } + + callerBtn.addEventListener("click", () => { + if (!callerBtn.disabled) setActive("caller"); + }); + callBtn.addEventListener("click", () => { + if (!callBtn.disabled) setActive("call"); + }); + }); +}); diff --git a/doc/doxygen/theme b/doc/doxygen/theme new file mode 160000 index 000000000..d52eafe3e --- /dev/null +++ b/doc/doxygen/theme @@ -0,0 +1 @@ +Subproject commit d52eafe3e9303399fda15661f3d7bb8fe3d7eabc diff --git a/docker-compose.yml b/docker-compose.yml index 3d9f9f38f..6cbac61f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: build: context: . dockerfile: Dockerfile - image: servatrice + image: ghcr.io/cockatrice/servatrice:latest depends_on: - mysql ports: diff --git a/docker-compose.yml.windows b/docker-compose.yml.windows index 6663c90fd..3d29b1e5f 100644 --- a/docker-compose.yml.windows +++ b/docker-compose.yml.windows @@ -16,7 +16,7 @@ services: build: context: . dockerfile: Dockerfile - image: servatrice + image: ghcr.io/cockatrice/servatrice:latest depends_on: - mysql ports: diff --git a/format.sh b/format.sh index 8c60f3f8a..f8c183dfc 100755 --- a/format.sh +++ b/format.sh @@ -2,7 +2,8 @@ # This script will run clang-format on all modified, non-3rd-party C++/Header files. # Optionally runs cmake-format on all modified cmake files. -# Uses clang-format cmake-format git diff find +# Optionally runs shellcheck on all modified shell files. +# Uses clang-format cmake-format git diff find shellcheck # Never, ever, should this receive a path with a newline in it. Don't bother proofing it for that. set -o pipefail @@ -12,25 +13,33 @@ olddir="$PWD" cd "${BASH_SOURCE%/*}/" || exit 2 # could not find path, this could happen with special links etc. # defaults -include=("common" \ -"cockatrice/src" \ -"dbconverter/src" \ +include=("cockatrice/src" \ +"libcockatrice_card" \ +"libcockatrice_deck_list" \ +"libcockatrice_network" \ +"libcockatrice_protocol" \ +"libcockatrice_rng" \ +"libcockatrice_settings" \ +"libcockatrice_utility" \ "oracle/src" \ "servatrice/src" \ "tests") -exclude=("servatrice/src/smtp" \ -"common/sfmt" \ -"common/lib" \ -"oracle/src/zip" \ -"oracle/src/lzma" \ -"oracle/src/qt-json") +exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ +"libcockatrice_utility/libcockatrice/utility/peglib.h" \ +"oracle/src/lzma/" \ +"oracle/src/qt-json/" \ +"oracle/src/zip/" \ +"servatrice/src/smtp/") exts=("cpp" "h" "proto") cf_cmd="clang-format" branch="origin/master" cmakefile="CMakeLists.txt" cmakedir="cmake/.*\\.cmake" cmakeinclude=("cmake/gtest-CMakeLists.txt.in") +scripts="*.sh" color="--" +verbosity=0 +sep="----------" # parse options while [[ $* ]]; do @@ -96,11 +105,21 @@ OPTIONS: -n, --names Display a list of filenames that require formatting. Implies --test. + --no-clang-format + Do not check any source files for clang-format. + + --print-version + Print the version of clang-format being used before continuing. + + --shell + Use shellcheck to lint shell files. Not available in the default inline + mode. + -t, --test Do not edit files in place. Set exit code to 1 if changes are required. - --cf-version - Print the version of clang-format being used before continuing. + -v, --verbose + Display output on successes. EXIT CODES: 0 on a successful format or if no files require formatting. @@ -117,7 +136,7 @@ EXAMPLES: Tests if the source files in the current directory are correctly formatted and prints an error message if formatting is required. - $0 --cmake --branch "" "" + $0 --cmake --branch "" --no-clang-format Unconditionally format all cmake files and no source files. EOM exit 0 @@ -126,12 +145,24 @@ EOM mode="name" shift ;; + '--no-clang-format') + include=() # do not check any dirs + shift + ;; + '--print-version') + print_version=1 + shift + ;; + '--shell') + do_shell=1 + shift + ;; '-t'|'--test') mode="code" shift ;; - '--cf-version') - print_version=1 + '-v'|'--verbose') + verbosity=1 shift ;; '--') @@ -185,6 +216,12 @@ if [[ $do_cmake ]] && ! hash cmake-format 2>/dev/null; then exit 3 fi +# check availability of shellcheck +if [[ $do_shell ]] && ! hash shellcheck 2>/dev/null; then + echo "could not find shellcheck" >&2 + exit 3 +fi + if [[ $branch ]]; then # get all dirty files through git if ! base=$(git merge-base "$branch" HEAD); then @@ -218,6 +255,15 @@ if [[ $branch ]]; then done done fi + if [[ $do_shell ]]; then + shell_names=() + for name in "${basenames[@]}"; do + filerx="(^|/)$scripts$" + if [[ $name =~ $filerx ]]; then + shell_names+=("$name") + fi + done + fi else exts_o=() for ext in "${exts[@]}"; do @@ -229,12 +275,15 @@ else mapfile -t cmake_names < <(find . -maxdepth 2 -type f -name "$cmakefile" -o -path "./${cmakedir/.}") cmake_names+=("${cmakeinclude[@]}") fi + if [[ $do_shell ]]; then + mapfile -t shell_names < <(find . -maxdepth 5 -type f -name "$scripts") + fi fi # filter excludes for path in "${exclude[@]}"; do for i in "${!names[@]}"; do - rx="^$path/" + rx="^$path" if [[ ${names[$i]} =~ $rx ]]; then unset "names[$i]" fi @@ -244,14 +293,18 @@ done # optionally print version if [[ $print_version ]]; then $cf_cmd -version - [[ $do_cmake ]] && echo "cmake-format $(cmake-format --version)" - echo "----------" + [[ $do_cmake ]] && echo "cmake-format version $(cmake-format --version)" + [[ $do_shell ]] && echo "shellcheck $(shellcheck --version | grep "version:")" + echo "$sep" fi if [[ ! ${cmake_names[*]} ]]; then unset do_cmake fi -if [[ ! ( ${names[*]} || $do_cmake ) ]]; then +if [[ ! ${shell_names[*]} ]]; then + unset do_shell +fi +if [[ ! ( ${names[*]} || $do_cmake || $do_shell ) ]]; then exit 0 # nothing to format means format is successful! fi @@ -259,16 +312,31 @@ fi case $mode in diff) declare -i code=0 + files_to_format=() for name in "${names[@]}"; do if ! $cf_cmd "$name" | diff "$name" - -p "$color"; then code=1 + files_to_format+=("$name") fi done for name in "${cmake_names[@]}"; do if ! cmake-format "$name" | diff "$name" - -p "$color"; then code=1 + files_to_format+=("$name") fi done + for name in "${shell_names[@]}"; do + if ! shellcheck "$name"; then + code=1 + files_to_format+=("$name") + fi + done + if (( code>0 )); then + echo "$sep" + for name in "${files_to_format[@]}"; do + echo "$name" + done + fi exit $code ;; name) @@ -285,6 +353,12 @@ case $mode in code=1 fi done + for name in "${shell_names[@]}"; do + if ! shellcheck "$name" >/dev/null; then + echo "$name" + code=1 + fi + done exit $code ;; code) @@ -294,6 +368,9 @@ case $mode in for name in "${cmake_names[@]}"; do cmake-format "$name" --check || exit 1 done + for name in "${shell_names[@]}"; do + shellcheck "$name" >/dev/null || exit 1 + done ;; *) if [[ "${names[*]}" ]]; then @@ -302,5 +379,16 @@ case $mode in if [[ $do_cmake ]]; then cmake-format -i "${cmake_names[@]}" fi + if [[ $do_shell ]]; then + echo "warning: --shell is not compatible with the current mode but shell files were modified!" >&2 + echo "recommendation: try $0 --diff --shell" >&2 + fi + if (( verbosity>0 )); then + count="${#names[*]}" + if [[ $do_cmake ]]; then + (( count+=${#cmake_names[*]} )) + fi + echo "parsed $count files that differ from base $branch" + fi ;; esac diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt new file mode 100644 index 000000000..dd3799e33 --- /dev/null +++ b/libcockatrice_card/CMakeLists.txt @@ -0,0 +1,61 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/card/card_info.h + libcockatrice/card/card_info_comparator.h + libcockatrice/card/database/card_database.h + libcockatrice/card/database/card_database_loader.h + libcockatrice/card/database/card_database_manager.h + libcockatrice/card/database/card_database_querier.h + libcockatrice/card/database/parser/card_database_parser.h + libcockatrice/card/database/parser/cockatrice_xml_3.h + libcockatrice/card/database/parser/cockatrice_xml_4.h + libcockatrice/card/import/card_name_normalizer.h + libcockatrice/card/printing/exact_card.h + libcockatrice/card/printing/printing_info.h + libcockatrice/card/set/card_set.h + libcockatrice/card/relation/card_relation.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_card STATIC + ${MOC_SOURCES} + libcockatrice/card/card_info.cpp + libcockatrice/card/card_info_comparator.cpp + libcockatrice/card/database/card_database.cpp + libcockatrice/card/database/card_database_loader.cpp + libcockatrice/card/database/card_database_manager.cpp + libcockatrice/card/database/card_database_querier.cpp + libcockatrice/card/database/parser/card_database_parser.cpp + libcockatrice/card/database/parser/cockatrice_xml_3.cpp + libcockatrice/card/database/parser/cockatrice_xml_4.cpp + libcockatrice/card/import/card_name_normalizer.cpp + libcockatrice/card/printing/exact_card.cpp + libcockatrice/card/printing/printing_info.cpp + libcockatrice/card/relation/card_relation.cpp + libcockatrice/card/set/card_set.cpp + libcockatrice/card/set/card_set_list.cpp + libcockatrice/card/format/format_legality_rules.cpp + libcockatrice/card/format/format_legality_rules.h +) + +target_include_directories( + libcockatrice_card + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR}/cockatrice/src/filters +) + +target_link_libraries( + libcockatrice_card + PUBLIC libcockatrice_interfaces + PUBLIC libcockatrice_utility + PUBLIC ${QT_CORE_MODULE} +) diff --git a/libcockatrice_card/libcockatrice/card/card_info.cpp b/libcockatrice_card/libcockatrice/card/card_info.cpp new file mode 100644 index 000000000..1aaec85b8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info.cpp @@ -0,0 +1,235 @@ +#include "card_info.h" + +#include "game_specific_terms.h" +#include "printing/printing_info.h" +#include "relation/card_relation.h" +#include "set/card_set.h" + +#include +#include +#include +#include +#include +#include +#include + +class CardRelation; +class CardSet; +class CardInfo; + +using CardInfoPtr = QSharedPointer; + +CardInfo::CardInfo(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + const UiAttributes _uiAttributes) + : name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards), + reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes) +{ + simpleName = CardInfo::simplifyName(name); + + refreshCachedSets(); +} + +CardInfoPtr CardInfo::newInstance(const QString &_name) +{ + return newInstance(_name, "", false, {}, {}, {}, {}, {}); +} + +CardInfoPtr CardInfo::newInstance(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + const UiAttributes _uiAttributes) +{ + CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, + _sets, _uiAttributes)); + ptr->setSmartPointer(ptr); + + for (const auto &printings : _sets) { + for (const PrintingInfo &printing : printings) { + printing.getSet()->append(ptr); + break; + } + } + + return ptr; +} + +QString CardInfo::getCorrectedName() const +{ + // remove all the characters reserved in windows file paths, + // other oses only disallow a subset of these so it covers all + static const QRegularExpression rmrx(R"(( // |[*<>:"\\?\x00-\x08\x10-\x1f]))"); + static const QRegularExpression spacerx(R"([/\x09-\x0f])"); + static const QString space(' '); + QString result = name; + // Fire // Ice, Circle of Protection: Red, "Ach! Hans, Run!", Who/What/When/Where/Why, Question Elemental? + return result.remove(rmrx).replace(spacerx, space); +} + +QString CardInfo::getLegalityProp(const QString &format) const +{ + return getProperty("format-" + format); +} + +bool CardInfo::isLegalInFormat(const QString &format) const +{ + if (format.isEmpty()) { + return true; + } + + QString formatLegality = getLegalityProp(format); + return formatLegality == "legal" || formatLegality == "restricted"; +} + +void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info) +{ + if (!_set->contains(smartThis)) { + _set->append(smartThis); + } + if (!setsToPrintings[_set->getShortName()].contains(_info)) { + setsToPrintings[_set->getShortName()].append(_info); + } + + refreshCachedSets(); +} + +void CardInfo::combineLegalities(const QVariantHash &props) +{ + QHashIterator it(props); + while (it.hasNext()) { + it.next(); + if (it.key().startsWith("format-")) { + smartThis->setProperty(it.key(), it.value().toString()); + } + } +} + +void CardInfo::refreshCachedSets() +{ + refreshCachedSetNames(); + refreshCachedAltNames(); +} + +void CardInfo::refreshCachedSetNames() +{ + QStringList setList; + // update the cached list of set names + for (const auto &printings : setsToPrintings) { + for (const auto &printing : printings) { + if (printing.getSet()->getEnabled()) { + setList << printing.getSet()->getShortName(); + } + break; + } + } + setsNames = setList.join(", "); +} + +void CardInfo::refreshCachedAltNames() +{ + altNames.clear(); + + // update the altNames with the flavorNames + for (const auto &printings : setsToPrintings) { + for (const auto &printing : printings) { + QString flavorName = printing.getFlavorName(); + if (!flavorName.isEmpty()) { + altNames.insert(flavorName); + } + } + } +} + +QString CardInfo::simplifyName(const QString &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"); + + // Replace Jötun Grunt with Jotun Grunt. + simpleName = simpleName.normalized(QString::NormalizationForm_KD); + + // remove all non alphanumeric characters from the name + simpleName.remove(nonAlnum); + return simpleName; +} + +QChar CardInfo::getColorChar() const +{ + QString colors = getColors(); + switch (colors.size()) { + case 0: + return QChar(); + case 1: + return colors.at(0); + default: + return QChar('m'); + } +} + +void CardInfo::resetReverseRelatedCards2Me() +{ + for (CardRelation *cardRelation : this->getReverseRelatedCards2Me()) { + cardRelation->deleteLater(); + } + reverseRelatedCardsToMe = QList(); +} + +// Back-compatibility methods. Remove ASAP +QString CardInfo::getCardType() const +{ + return getProperty(Mtg::CardType); +} +void CardInfo::setCardType(const QString &value) +{ + setProperty(Mtg::CardType, value); +} +QString CardInfo::getCmc() const +{ + return getProperty(Mtg::ConvertedManaCost); +} +QString CardInfo::getColors() const +{ + return getProperty(Mtg::Colors); +} +void CardInfo::setColors(const QString &value) +{ + setProperty(Mtg::Colors, value); +} +QString CardInfo::getLoyalty() const +{ + return getProperty(Mtg::Loyalty); +} +QString CardInfo::getMainCardType() const +{ + return getProperty(Mtg::MainCardType); +} +QString CardInfo::getManaCost() const +{ + return getProperty(Mtg::ManaCost); +} +QString CardInfo::getPowTough() const +{ + return getProperty(Mtg::PowTough); +} +void CardInfo::setPowTough(const QString &value) +{ + setProperty(Mtg::PowTough, value); +} diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h new file mode 100644 index 000000000..654ac1f63 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -0,0 +1,377 @@ +#ifndef CARD_INFO_H +#define CARD_INFO_H + +#include "format/format_legality_rules.h" +#include "printing/printing_info.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardInfoLog, "card_info"); + +class CardInfo; +class CardSet; +class CardRelation; +class ICardDatabaseParser; + +typedef QSharedPointer CardInfoPtr; +typedef QSharedPointer CardSetPtr; +typedef QSharedPointer FormatRulesPtr; +typedef QMap> SetToPrintingsMap; + +typedef QHash CardNameMap; +typedef QHash SetNameMap; +typedef QHash FormatRulesNameMap; + +Q_DECLARE_METATYPE(CardInfoPtr) + +/** + * @class CardInfo + * @ingroup Cards + * + * @brief Represents a card and its associated metadata, properties, and relationships. + * + * CardInfo holds both static information (name, text, flags) and dynamic data + * (properties, set memberships, relationships). It also integrates with + * signals/slots, allowing observers to react to property or visual updates. + * + * Each CardInfo may belong to multiple sets through its printings, and can + * be related to other cards through defined relationships. + */ +class CardInfo : public QObject +{ + Q_OBJECT + +public: + /** + * @class CardInfo::UiAttributes + * @ingroup Cards + * + * @brief Attributes of the card that affect display and game logic. + */ + struct UiAttributes + { + bool cipt = false; ///< Positioning flag used by UI. + bool landscapeOrientation = false; ///< Orientation flag for rendering. + int tableRow = 0; ///< Row index in a table or visual representation. + bool upsideDownArt = false; ///< Whether artwork is flipped for visual purposes. + }; + +private: + /** @name Private Card Properties + * @anchor PrivateCardProperties + */ + ///@{ + CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references. + QString name; ///< Full name of the card. + QString simpleName; ///< Simplified name for fuzzy matching. + QString text; ///< Text description or rules text of the card. + bool isToken; ///< Whether this card is a token or not. + QVariantHash properties; ///< Key-value store of dynamic card properties. + QList relatedCards; ///< Forward references to related cards. + QList reverseRelatedCards; ///< Cards that refer back to this card. + QList reverseRelatedCardsToMe; ///< Cards that consider this card as related. + SetToPrintingsMap setsToPrintings; ///< Mapping from set names to printing variations. + UiAttributes uiAttributes; ///< Attributes that affect display and game logic + QString setsNames; ///< Cached, human-readable list of set names. + QSet altNames; ///< Cached set of alternate names, used when searching + ///@} + +public: + /** + * @brief Constructs a CardInfo with full initialization. + * + * @param _name The name of the card. + * @param _text Rules text or description of the card. + * @param _isToken Flag indicating whether the card is a token. + * @param _properties Arbitrary key-value properties. + * @param _relatedCards Forward references to related cards. + * @param _reverseRelatedCards Backward references to related cards. + * @param _sets Map of set names to printing information. + * @param _uiAttributes Attributes that affect display and game logic + */ + explicit CardInfo(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + UiAttributes _uiAttributes); + + /** + * @brief Copy constructor for CardInfo. + * + * Performs a deep copy of properties, sets, and related card lists. + * + * @param other Another CardInfo to copy. + */ + CardInfo(const CardInfo &other) + : QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text), + isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards), + reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe), + setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames), + altNames(other.altNames) + { + } + + /** + * @brief Creates a new instance with only the card name. + * + * All other fields are set to defaults. + * + * @param _name The card name. + * @return Shared pointer to the new CardInfo instance. + */ + static CardInfoPtr newInstance(const QString &_name); + + /** + * @brief Creates a new instance with full initialization. + * + * @param _name Name of the card. + * @param _text Rules text or description. + * @param _isToken Token flag. + * @param _properties Arbitrary properties. + * @param _relatedCards Forward relationships. + * @param _reverseRelatedCards Reverse relationships. + * @param _sets Printing information per set. + * @param _uiAttributes Attributes that affect display and game logic + * @return Shared pointer to the new CardInfo instance. + */ + static CardInfoPtr newInstance(const QString &_name, + const QString &_text, + bool _isToken, + QVariantHash _properties, + const QList &_relatedCards, + const QList &_reverseRelatedCards, + SetToPrintingsMap _sets, + UiAttributes _uiAttributes); + + /** + * @brief Clones the current CardInfo instance. + * + * Uses the copy constructor and ensures the smart pointer is properly set. + * + * @return Shared pointer to the cloned CardInfo. + */ + [[nodiscard]] CardInfoPtr clone() const + { + auto newCardInfo = CardInfoPtr(new CardInfo(*this)); + newCardInfo->setSmartPointer(newCardInfo); // Set the smart pointer for the new instance + return newCardInfo; + } + + /** + * @brief Sets the internal smart pointer to self. + * + * Used internally to allow safe cross-references among CardInfo and CardSet. + * + * @param _ptr Shared pointer pointing to this instance. + */ + void setSmartPointer(CardInfoPtr _ptr) + { + smartThis = std::move(_ptr); + } + + /** @name Basic Properties Accessors */ //@{ + [[nodiscard]] inline const QString &getName() const + { + return name; + } + [[nodiscard]] const QString &getSimpleName() const + { + return simpleName; + } + const QSet &getAltNames() + { + return altNames; + } + [[nodiscard]] const QString &getText() const + { + return text; + } + void setText(const QString &_text) + { + text = _text; + emit cardInfoChanged(smartThis); + } + [[nodiscard]] bool getIsToken() const + { + return isToken; + } + [[nodiscard]] QStringList getProperties() const + { + return properties.keys(); + } + [[nodiscard]] QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); + emit cardInfoChanged(smartThis); + } + [[nodiscard]] bool hasProperty(const QString &propertyName) const + { + return properties.contains(propertyName); + } + [[nodiscard]] const SetToPrintingsMap &getSets() const + { + return setsToPrintings; + } + [[nodiscard]] const QString &getSetsNames() const + { + return setsNames; + } + //@} + + /** @name Related Cards Accessors */ //@{ + [[nodiscard]] const QList &getRelatedCards() const + { + return relatedCards; + } + [[nodiscard]] const QList &getReverseRelatedCards() const + { + return reverseRelatedCards; + } + [[nodiscard]] const QList &getReverseRelatedCards2Me() const + { + return reverseRelatedCardsToMe; + } + [[nodiscard]] QList getAllRelatedCards() const + { + QList result; + result.append(getRelatedCards()); + result.append(getReverseRelatedCards2Me()); + return result; + } + void resetReverseRelatedCards2Me(); + void addReverseRelatedCards2Me(CardRelation *cardRelation) + { + reverseRelatedCardsToMe.append(cardRelation); + } + //@} + + /** @name UI Positioning */ //@{ + [[nodiscard]] const UiAttributes &getUiAttributes() const + { + return uiAttributes; + } + //@} + + [[nodiscard]] QChar getColorChar() const; + + /** @name Legacy/Convenience Property Accessors */ //@{ + [[nodiscard]] QString getCardType() const; + void setCardType(const QString &value); + [[nodiscard]] QString getCmc() const; + [[nodiscard]] QString getColors() const; + void setColors(const QString &value); + [[nodiscard]] QString getLoyalty() const; + [[nodiscard]] QString getMainCardType() const; + [[nodiscard]] QString getManaCost() const; + [[nodiscard]] QString getPowTough() const; + void setPowTough(const QString &value); + //@} + + /** + * @brief Returns a version of the card name safe for file storage or fuzzy matching. + * + * Removes invalid characters, replaces spacing markers, and normalizes diacritics. + * + * @return Corrected card name as a QString. + */ + [[nodiscard]] QString getCorrectedName() const; + + /** + * @brief Gets the card's legality value for the given format. + * The legality prop for a format is stored in the property map under the key "format-" + * @param format The format's name. + * @return The card's legality value for the format. Empty if not found. + */ + [[nodiscard]] QString getLegalityProp(const QString &format) const; + + /** + * @brief Checks if the card is legal in the given format. + * A card is considered legal in a format if its properties map contains an entry for "format-", with value + * "legal" or "restricted". + * @param format The format's name. If empty, will always return true. + * @return Whether the card is legal in the given format. + */ + [[nodiscard]] bool isLegalInFormat(const QString &format) const; + + /** + * @brief Adds a printing to a specific set. + * + * Updates the mapping and refreshes the cached list of set names. + * + * @param _set The set to which the card should be added. + * @param _info Optional printing information. + */ + void addToSet(const CardSetPtr &_set, const PrintingInfo &_info = PrintingInfo()); + + /** + * @brief Combines legality properties from a provided map. + * + * Useful for merging format legality flags from multiple sources. + * + * @param props Key-value mapping of format legalities. + */ + void combineLegalities(const QVariantHash &props); + + /** + * @brief Refreshes all cached fields that are calculated from the contained sets and printings. + * + * Typically called after adding or modifying set memberships or printings. + */ + void refreshCachedSets(); + + /** + * @brief Simplifies a name for fuzzy matching. + * + * Converts to lowercase, removes punctuation/spacing. + * + * @param name Original name string. + * @return Simplified name string. + */ + static QString simplifyName(const QString &name); + +private: + /** + * @brief Refreshes the cached, human-readable list of set names. + * + * Typically called after adding or modifying set memberships. + */ + void refreshCachedSetNames(); + + /** + * @brief Refreshes the cached list of alt names for the card. + * + * Typically called after adding or modifying the contained printings. + */ + void refreshCachedAltNames(); + +signals: + /** + * @brief Emitted when a pixmap for this card has been updated or finished loading. + * + * @param printing Specific printing for which the pixmap has updated. + */ + void pixmapUpdated(const PrintingInfo &printing); + + /** + * @brief Emitted when card properties or state have changed. + * + * @param card Shared pointer to the CardInfo instance that changed. + */ + void cardInfoChanged(CardInfoPtr card); +}; +#endif diff --git a/cockatrice/src/utility/card_info_comparator.cpp b/libcockatrice_card/libcockatrice/card/card_info_comparator.cpp similarity index 100% rename from cockatrice/src/utility/card_info_comparator.cpp rename to libcockatrice_card/libcockatrice/card/card_info_comparator.cpp diff --git a/cockatrice/src/utility/card_info_comparator.h b/libcockatrice_card/libcockatrice/card/card_info_comparator.h similarity index 60% rename from cockatrice/src/utility/card_info_comparator.h rename to libcockatrice_card/libcockatrice/card/card_info_comparator.h index 9402ed401..b8d9c47e6 100644 --- a/cockatrice/src/utility/card_info_comparator.h +++ b/libcockatrice_card/libcockatrice/card/card_info_comparator.h @@ -1,9 +1,14 @@ +/** + * @file card_info_comparator.h + * @ingroup Cards + * @brief TODO: Document this. + */ + #ifndef CARD_INFO_COMPARATOR_H #define CARD_INFO_COMPARATOR_H -#include "../game/cards/card_info.h" +#include "card_info.h" -#include #include #include @@ -17,8 +22,8 @@ private: QStringList m_properties; // List of properties to sort by Qt::SortOrder m_order; - QVariant getProperty(const CardInfoPtr &card, const QString &property) const; - bool compareVariants(const QVariant &a, const QVariant &b) const; + [[nodiscard]] QVariant getProperty(const CardInfoPtr &card, const QString &property) const; + [[nodiscard]] bool compareVariants(const QVariant &a, const QVariant &b) const; }; #endif // CARD_INFO_COMPARATOR_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp new file mode 100644 index 000000000..5c4b408b3 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -0,0 +1,206 @@ +#include "card_database.h" + +#include "../relation/card_relation.h" +#include "parser/cockatrice_xml_4.h" + +#include +#include +#include +#include +#include +#include +#include + +CardDatabase::CardDatabase(QObject *parent, + ICardPreferenceProvider *prefs, + ICardDatabasePathProvider *pathProvider, + ICardSetPriorityController *_setPriorityController) + : QObject(parent), setPriorityController(_setPriorityController), loadStatus(NotLoaded) +{ + qRegisterMetaType("CardInfoPtr"); + qRegisterMetaType("CardSetPtr"); + + // create loader and wire it up + loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController); + // re-emit loader signals (so other code doesn't need to know about internals) + connect(loader, &CardDatabaseLoader::loadingFinished, this, &CardDatabase::cardDatabaseLoadingFinished); + connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed); + connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound); + connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled); + + querier = new CardDatabaseQuerier(this, this, prefs); +} + +CardDatabase::~CardDatabase() +{ + clear(); +} + +void CardDatabase::clear() +{ + QMutexLocker locker(clearDatabaseMutex); + + for (const auto &card : cards.values()) { + if (card) { + removeCard(card); + } + } + + cards.clear(); + simpleNameCards.clear(); + + sets.clear(); + ICardDatabaseParser::clearSetlist(); + + loadStatus = NotLoaded; +} + +void CardDatabase::loadCardDatabases() +{ + loadStatus = loader->loadCardDatabases(); +} + +bool CardDatabase::saveCustomTokensToFile() +{ + return loader->saveCustomTokensToFile(); +} + +void CardDatabase::refreshCachedReverseRelatedCards() +{ + for (const auto &card : cards) { + card->resetReverseRelatedCards2Me(); + } + + for (const auto &card : cards) { + for (auto *rel : card->getReverseRelatedCards()) { + if (auto target = cards.value(rel->getName())) { + auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), + rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); + target->addReverseRelatedCards2Me(newRel); + } + } + } +} + +void CardDatabase::addCard(CardInfoPtr card) +{ + if (card == nullptr) { + qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)"; + return; + } + + auto name = card->getName(); + + // If a card already exists, just add the new set property. + if (auto existing = cards.value(name)) { + for (const auto &printings : card->getSets()) + for (const auto &printing : printings) + existing->addToSet(printing.getSet(), printing); + return; + } + + QMutexLocker locker(addCardMutex); + cards.insert(name, card); + simpleNameCards.insert(card->getSimpleName(), card); + + emit cardAdded(card); +} + +void CardDatabase::removeCard(CardInfoPtr card) +{ + if (card.isNull()) { + qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)"; + return; + } + + for (auto *cardRelation : card->getRelatedCards()) + cardRelation->deleteLater(); + + for (auto *cardRelation : card->getReverseRelatedCards()) + cardRelation->deleteLater(); + + for (auto *cardRelation : card->getReverseRelatedCards2Me()) + cardRelation->deleteLater(); + + QMutexLocker locker(removeCardMutex); + cards.remove(card->getName()); + simpleNameCards.remove(card->getSimpleName()); + emit cardRemoved(card); +} + +void CardDatabase::addSet(CardSetPtr set) +{ + sets.insert(set->getShortName(), set); +} + +CardSetPtr CardDatabase::getSet(const QString &setName) +{ + if (sets.contains(setName)) { + return sets.value(setName); + } else { + CardSetPtr newSet = CardSet::newInstance(setPriorityController, setName); + sets.insert(setName, newSet); + return newSet; + } +} + +CardSetList CardDatabase::getSetList() const +{ + CardSetList result; + for (auto set : sets.values()) { + result << set; + } + return result; +} + +void CardDatabase::checkUnknownSets() +{ + auto _sets = getSetList(); + + if (_sets.getEnabledSetsNum()) { + // if some sets are first found on this run, ask the user + int numUnknownSets = _sets.getUnknownSetsNum(); + QStringList unknownSetNames = _sets.getUnknownSetsNames(); + if (numUnknownSets > 0) { + emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames); + } else { + _sets.markAllAsKnown(); + } + } else { + // No set enabled. Probably this is the first time running trice + _sets.guessSortKeys(); + _sets.sortByKey(); + _sets.enableAll(); + notifyEnabledSetsChanged(); + + emit cardDatabaseAllNewSetsEnabled(); + } +} + +void CardDatabase::enableAllUnknownSets() +{ + auto _sets = getSetList(); + _sets.enableAllUnknown(); +} + +void CardDatabase::markAllSetsAsKnown() +{ + auto _sets = getSetList(); + _sets.markAllAsKnown(); +} + +void CardDatabase::notifyEnabledSetsChanged() +{ + // refresh the list of cached set names + for (const CardInfoPtr &card : cards) { + card->refreshCachedSets(); + } + + // inform the carddatabasemodels that they need to re-check their list of cards + emit cardDatabaseEnabledSetsChanged(); +} + +void CardDatabase::addFormat(FormatRulesPtr format) +{ + formats.insert(format->formatName.toLower(), format); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h new file mode 100644 index 000000000..7f8fc39db --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -0,0 +1,186 @@ +#ifndef CARDDATABASE_H +#define CARDDATABASE_H + +#include "../set/card_set_list.h" +#include "card_database_loader.h" +#include "card_database_querier.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardDatabaseLog, "card_database"); + +/** + * @class CardDatabase + * @ingroup CardDatabase + * @brief Core in-memory container for card and set data. + * + * Responsible for maintaining CardInfo objects, CardSet objects, and + * providing access to CardDatabaseQuerier for query operations. + * Handles addition, removal, and clearing of cards and sets. + */ +class CardDatabase : public QObject +{ + Q_OBJECT + +protected: + /// Controller to determine set priority when choosing preferred printings. + ICardSetPriorityController *setPriorityController; + + /// Cards indexed by exact name + CardNameMap cards; + + /// Cards indexed by simplified name (normalized) + CardNameMap simpleNameCards; + + /// Sets indexed by short name + SetNameMap sets; + + FormatRulesNameMap formats; + + /// Loader responsible for file discovery and parsing + CardDatabaseLoader *loader; + + /// Current load status of the database + LoadStatus loadStatus; + + /// Querier for higher-level card lookups + CardDatabaseQuerier *querier; + +private: + /** + * @brief Check for sets that are unknown and emit signals if needed. + */ + void checkUnknownSets(); + + /** + * @brief Refreshes the cached reverse-related cards for all cards. + */ + void refreshCachedReverseRelatedCards(); + + /// Mutexes for thread safety + QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(), + *removeCardMutex = new QBasicMutex(); + +public: + /** + * @brief Constructs a new CardDatabase instance. + * @param parent QObject parent. + * @param prefs Optional card preference provider. + * @param pathProvider Optional database path provider. + * @param setPriorityController Optional controller for set priority. + */ + explicit CardDatabase(QObject *parent = nullptr, + ICardPreferenceProvider *prefs = nullptr, + ICardDatabasePathProvider *pathProvider = nullptr, + ICardSetPriorityController *setPriorityController = nullptr); + + /** @brief Destructor clears all internal data. */ + ~CardDatabase() override; + + /** + * @brief Removes a card from the database. + * @param card Pointer to the card to remove. + */ + void removeCard(CardInfoPtr card); + + /** @brief Clears all cards, sets, and internal state. */ + void clear(); + + /** @brief Returns the map of cards by name. */ + [[nodiscard]] const CardNameMap &getCardList() const + { + return cards; + } + + /** + * @brief Retrieves a set by short name, creating a new one if missing. + * @param setName Short name of the set. + * @return Pointer to the CardSet. + */ + CardSetPtr getSet(const QString &setName); + + /** @brief Returns a list of all sets in the database. */ + [[nodiscard]] CardSetList getSetList() const; + + /** @brief Returns the current load status. */ + [[nodiscard]] LoadStatus getLoadStatus() const + { + return loadStatus; + } + + /** @brief Returns the querier for performing card lookups. */ + [[nodiscard]] CardDatabaseQuerier *query() const + { + return querier; + } + + /** @brief Enables all unknown sets in the database. */ + void enableAllUnknownSets(); + + /** @brief Marks all sets as known. */ + void markAllSetsAsKnown(); + + /** @brief Notifies listeners that enabled sets changed. */ + void notifyEnabledSetsChanged(); + +public slots: + /** + * @brief Adds a card to the database. + * @param card CardInfoPtr to add. + */ + void addCard(CardInfoPtr card); + + /** + * @brief Adds a set to the database. + * @param set Pointer to CardSet to add. + */ + void addSet(CardSetPtr set); + + void addFormat(FormatRulesPtr format); + + /** @brief Loads card databases from configured paths. */ + void loadCardDatabases(); + + /** @brief Saves custom tokens to file. + * @return True if successful. + */ + bool saveCustomTokensToFile(); + +signals: + /** @brief Emitted when the card database has finished loading successfully. */ + void cardDatabaseLoadingFinished(); + + /** @brief Emitted when the card database fails to load. */ + void cardDatabaseLoadingFailed(); + + /** + * @brief Emitted when new sets are found. + * @param numUnknownSets Number of unknown sets. + * @param unknownSetsNames Names of unknown sets. + */ + void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames); + + /** @brief Emitted when all new sets have been enabled. */ + void cardDatabaseAllNewSetsEnabled(); + + /** @brief Emitted when enabled sets have changed. */ + void cardDatabaseEnabledSetsChanged(); + + /** @brief Emitted when a new card is added. */ + void cardAdded(CardInfoPtr card); + + /** @brief Emitted when a card is removed. */ + void cardRemoved(CardInfoPtr card); + + friend class CardDatabaseLoader; + friend class CardDatabaseQuerier; +}; + +#endif // CARDDATABASE_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp new file mode 100644 index 000000000..716477a59 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp @@ -0,0 +1,156 @@ +#include "card_database_loader.h" + +#include "card_database.h" +#include "parser/cockatrice_xml_3.h" +#include "parser/cockatrice_xml_4.h" + +#include +#include +#include +#include + +CardDatabaseLoader::CardDatabaseLoader(QObject *parent, + CardDatabase *db, + ICardDatabasePathProvider *_pathProvider, + ICardPreferenceProvider *_preferenceProvider, + ICardSetPriorityController *_priorityController) + : QObject(parent), database(db), pathProvider(_pathProvider) +{ + // instantiate available parsers here and connect them to the database + availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController); + availableParsers << new CockatriceXml3Parser(_priorityController); + + for (auto *p : availableParsers) { + // connect parser outputs to the database adders + connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection); + connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection); + connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection); + } + + // when SettingsCache's path changes, trigger reloads + connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, + &CardDatabaseLoader::loadCardDatabases); +} + +CardDatabaseLoader::~CardDatabaseLoader() +{ + qDeleteAll(availableParsers); + availableParsers.clear(); +} + +LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + return FileError; + } + + for (auto parser : availableParsers) { + file.reset(); + if (parser->getCanParseFile(fileName, file)) { + file.reset(); + parser->parseFile(file); + return Ok; + } + } + + return Invalid; +} + +LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path) +{ + auto startTime = QTime::currentTime(); + LoadStatus tempLoadStatus = NotLoaded; + if (!path.isEmpty()) { + QMutexLocker locker(loadFromFileMutex); + tempLoadStatus = loadFromFile(path); + } + + int msecs = startTime.msecsTo(QTime::currentTime()); + qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus + << "Cards =" << (database ? database->cards.size() : 0) + << "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs); + + return tempLoadStatus; +} + +LoadStatus CardDatabaseLoader::loadCardDatabases() +{ + QMutexLocker locker(reloadDatabaseMutex); + + if (!database) { + qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer"; + emit loadingFailed(); + return FileError; + } + emit loadingStarted(); + qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started"; + + database->clear(); // remove old db + + LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database + loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database + loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database + + // find all custom card databases, recursively & following symlinks + // then load them alphabetically + const QStringList customPaths = collectCustomDatabasePaths(); + for (int i = 0; i < customPaths.size(); ++i) { + const auto &p = customPaths.at(i); + qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")"; + loadCardDatabase(p); + } + + // AFTER all the cards have been loaded + + // resolve the reverse-related tags + + database->refreshCachedReverseRelatedCards(); + + if (loadStatus == Ok) { + database->checkUnknownSets(); // update deck editors, etc + qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success"; + emit loadingFinished(); + } else { + qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed"; + emit loadingFailed(); // bring up the settings dialog + } + + return loadStatus; +} + +QStringList CardDatabaseLoader::collectCustomDatabasePaths() const +{ + QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files, + QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + + QStringList paths; + while (it.hasNext()) + paths << it.next(); + paths.sort(); + return paths; +} + +bool CardDatabaseLoader::saveCustomTokensToFile() +{ + if (!database) { + qCWarning(CardDatabaseLog) << "saveCustomTokensToFile: database pointer missing"; + return false; + } + + QString fileName = pathProvider->getCustomCardDatabasePath() + "/" + CardSet::TOKENS_SETNAME + ".xml"; + + SetNameMap tmpSets; + CardSetPtr customTokensSet = database->getSet(CardSet::TOKENS_SETNAME); + tmpSets.insert(CardSet::TOKENS_SETNAME, customTokensSet); + + CardNameMap tmpCards; + for (const CardInfoPtr &card : database->cards) { + if (card->getSets().contains(CardSet::TOKENS_SETNAME)) { + tmpCards.insert(card->getName(), card); + } + } + + availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName); + return true; +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.h b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h new file mode 100644 index 000000000..861cc95b0 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.h @@ -0,0 +1,125 @@ +#ifndef COCKATRICE_CARD_DATABASE_LOADER_H +#define COCKATRICE_CARD_DATABASE_LOADER_H + +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CardDatabaseLoadingLog, "card_database.loading"); +inline Q_LOGGING_CATEGORY(CardDatabaseLoadingSuccessOrFailureLog, "card_database.loading.success_or_failure"); + +class CardDatabase; +class ICardDatabaseParser; + +/** + * @enum LoadStatus + * @brief Represents the result of attempting to load a card database. + */ +enum LoadStatus +{ + Ok, /**< Database loaded successfully. */ + VersionTooOld, /**< Database version is too old to load. */ + Invalid, /**< Database is invalid or unparsable. */ + NotLoaded, /**< Database has not been loaded. */ + FileError, /**< Error opening or reading the file. */ + NoCards /**< Database contains no cards. */ +}; + +/** + * @class CardDatabaseLoader + * @ingroup CardDatabase + * @brief Handles loading card databases from disk and saving custom tokens. + * + * This class is responsible for: + * - Discovering configured card database paths. + * - Loading main, token, spoiler, and custom databases. + * - Populating a CardDatabase instance using connected parsers. + * - Emitting signals about loading progress and new sets. + */ +class CardDatabaseLoader : public QObject +{ + Q_OBJECT +public: + /** + * @brief Constructs a CardDatabaseLoader. + * @param parent QObject parent. + * @param db Pointer to the CardDatabase to populate (non-owning). + * @param pathProvider Provider for card database file paths. + * @param preferenceProvider Optional card preference provider for pinned printings. + */ + explicit CardDatabaseLoader(QObject *parent, + CardDatabase *db, + ICardDatabasePathProvider *pathProvider, + ICardPreferenceProvider *preferenceProvider, + ICardSetPriorityController *_priorityController); + + /** @brief Destructor cleans up allocated parsers. */ + ~CardDatabaseLoader() override; + +public slots: + /** + * @brief Loads all configured card databases. + * @return Status of the main database load. + */ + LoadStatus loadCardDatabases(); + + /** + * @brief Loads a single card database file. + * @param path Path to the database file. + * @return LoadStatus indicating success or failure. + */ + LoadStatus loadCardDatabase(const QString &path); + + /** + * @brief Saves custom tokens to the user-defined custom database path. + * @return True if the save was successful. + */ + bool saveCustomTokensToFile(); + +signals: + /** @brief Emitted when loading starts. */ + void loadingStarted(); + + /** @brief Emitted when loading finishes successfully. */ + void loadingFinished(); + + /** @brief Emitted when loading fails. */ + void loadingFailed(); + + /** + * @brief Emitted when new sets are discovered during loading. + * @param numSets Number of new sets. + * @param setNames Names of the discovered sets. + */ + void newSetsFound(int numSets, const QStringList &setNames); + + /** @brief Emitted when all newly discovered sets have been enabled. */ + void allNewSetsEnabled(); + +private: + /** + * @brief Loads a database from a single file using the available parsers. + * @param fileName Path to the database file. + * @return LoadStatus indicating success or failure. + */ + LoadStatus loadFromFile(const QString &fileName); + + /** + * @brief Collects custom card database paths recursively. + * @return Sorted list of file paths to custom databases. + */ + [[nodiscard]] QStringList collectCustomDatabasePaths() const; + +private: + CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */ + ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */ + QList availableParsers; /**< List of available parsers for different formats. */ + + QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */ + QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */ +}; + +#endif // COCKATRICE_CARD_DATABASE_LOADER_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp new file mode 100644 index 000000000..614b4a7f8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_manager.cpp @@ -0,0 +1,35 @@ +#include "card_database_manager.h" + +#include +#include +#include + +ICardPreferenceProvider *CardDatabaseManager::cardPreferenceProvider = new NoopCardPreferenceProvider(); +ICardDatabasePathProvider *CardDatabaseManager::pathProvider = new NoopCardDatabasePathProvider(); +ICardSetPriorityController *CardDatabaseManager::setPriorityController = new NoopCardSetPriorityController(); + +void CardDatabaseManager::setCardPreferenceProvider(ICardPreferenceProvider *provider) +{ + cardPreferenceProvider = provider; +} + +void CardDatabaseManager::setCardDatabasePathProvider(ICardDatabasePathProvider *provider) +{ + pathProvider = provider; +} + +void CardDatabaseManager::setCardSetPriorityController(ICardSetPriorityController *controller) +{ + setPriorityController = controller; +} + +CardDatabase *CardDatabaseManager::getInstance() +{ + static CardDatabase instance(nullptr, cardPreferenceProvider, pathProvider, setPriorityController); + return &instance; +} + +CardDatabaseQuerier *CardDatabaseManager::query() +{ + return getInstance()->query(); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_manager.h b/libcockatrice_card/libcockatrice/card/database/card_database_manager.h new file mode 100644 index 000000000..58a744fbb --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_manager.h @@ -0,0 +1,80 @@ +#ifndef CARD_DATABASE_ACCESSOR_H +#define CARD_DATABASE_ACCESSOR_H + +#pragma once +#include "card_database.h" + +/** + * @class CardDatabaseManager + * @ingroup CardDatabase + * @brief The CardDatabaseManager is responsible for managing the global CardDatabase singleton. + * + * This class provides a static interface for accessing the global CardDatabase instance + * and its CardDatabaseQuerier. It also allows the configuration of optional providers: + * - ICardPreferenceProvider + * - ICardDatabasePathProvider + * - ICardSetPriorityController + * + * Only a single instance of CardDatabase exists, enforced via a private constructor and + * deleted copy/move operations. + */ +class CardDatabaseManager +{ +public: + /** @brief Deleted copy constructor to enforce singleton. */ + CardDatabaseManager(const CardDatabaseManager &) = delete; + + /** @brief Deleted assignment operator to enforce singleton. */ + CardDatabaseManager &operator=(const CardDatabaseManager &) = delete; + + /** + * @brief Sets the card preference provider. + * @param provider Pointer to an ICardPreferenceProvider. + * @note Must be called before the first call to getInstance(). + */ + static void setCardPreferenceProvider(ICardPreferenceProvider *provider); + + /** + * @brief Sets the card database path provider. + * @param provider Pointer to an ICardDatabasePathProvider. + * @note Must be called before the first call to getInstance(). + */ + static void setCardDatabasePathProvider(ICardDatabasePathProvider *provider); + + /** + * @brief Sets the card set priority controller. + * @param controller Pointer to an ICardSetPriorityController. + * @note Must be called before the first call to getInstance(). + */ + static void setCardSetPriorityController(ICardSetPriorityController *controller); + + /** + * @brief Returns the singleton CardDatabase instance. + * @return Pointer to the global CardDatabase. + */ + static CardDatabase *getInstance(); + + /** + * @brief Returns the CardDatabaseQuerier of the singleton database. + * @return Pointer to CardDatabaseQuerier. + */ + static CardDatabaseQuerier *query(); + +private: + /** @brief Private default constructor to enforce singleton. */ + CardDatabaseManager() = default; + + /** @brief Private destructor. */ + ~CardDatabaseManager() = default; + + /// Static card preference provider pointer (default: Noop) + static ICardPreferenceProvider *cardPreferenceProvider; + + /// Static path provider pointer (default: Noop) + static ICardDatabasePathProvider *pathProvider; + + /// Static set priority controller pointer (default: Noop) + static ICardSetPriorityController *setPriorityController; +}; + +#endif // CARD_DATABASE_ACCESSOR_H diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp new file mode 100644 index 000000000..021e8d12d --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -0,0 +1,363 @@ +#include "card_database_querier.h" + +#include "../card_info.h" +#include "../printing/exact_card.h" +#include "../set/card_set_comparator.h" +#include "card_database.h" + +#include + +CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent, + const CardDatabase *_db, + const ICardPreferenceProvider *prefs) + : QObject(_parent), db(_db), prefs(prefs) +{ +} + +/** + * Looks up the cardInfo corresponding to the cardName. + * + * @param cardName The card name to look up + * @return A CardInfoPtr, or null if not corresponding CardInfo is found. + */ +CardInfoPtr CardDatabaseQuerier::getCardInfo(const QString &cardName) const +{ + return db->cards.value(cardName); +} + +/** + * Looks up the cardInfos for a list of card names. + * + * @param cardNames The card names to look up + * @return A List of CardInfoPtr. Any failed lookups will be ignored and dropped from the resulting list + */ +QList CardDatabaseQuerier::getCardInfos(const QStringList &cardNames) const +{ + QList cardInfos; + for (const QString &cardName : cardNames) { + CardInfoPtr ptr = db->cards.value(cardName); + if (ptr) + cardInfos.append(ptr); + } + + return cardInfos; +} + +CardInfoPtr CardDatabaseQuerier::getCardBySimpleName(const QString &cardName) const +{ + return db->simpleNameCards.value(CardInfo::simplifyName(cardName)); +} + +CardInfoPtr CardDatabaseQuerier::lookupCardByName(const QString &name) const +{ + if (auto info = getCardInfo(name)) + return info; + if (auto info = getCardBySimpleName(name)) + return info; + return getCardBySimpleName(CardInfo::simplifyName(name)); +} + +/** + * Looks up the cards corresponding to the CardRefs. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRefs The cards to look up. If providerId is empty for an entry, will default to the preferred printing for + * that entry. If providerId is given but not found, the PrintingInfo will be empty for that entry. + * @return A list of cards. Any failed lookups will be ignored and dropped from the resulting list. + */ +QList CardDatabaseQuerier::getCards(const QList &cardRefs) const +{ + QList cards; + for (const auto &cardRef : cardRefs) { + ExactCard card = getCard(cardRef); + if (card) + cards.append(card); + } + + return cards; +} + +/** + * Looks up the card corresponding to the CardRef. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRef The card to look up. + * @return A specific printing of a card, or empty if not found. + */ +ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const +{ + auto info = getCardInfo(cardRef.name); + if (info.isNull()) { + return {}; + } + + if (cardRef.providerId.isEmpty() || cardRef.providerId.isNull()) { + return ExactCard(info, getPreferredPrinting(info)); + } + + return ExactCard(info, findPrintingWithId(info, cardRef.providerId)); +} + +/** + * Looks up the card by CardRef, simplifying the name if required. + * If the providerId is empty, will default to the preferred printing. + * If providerId is given but not found, the PrintingInfo will be empty. + * + * @param cardRef The card to look up. + * @return A specific printing of a card, or empty if not found. + */ +ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const +{ + auto card = lookupCardByName(cardRef.name); + auto printing = + cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId); + + return ExactCard(card, printing); +} + +ExactCard CardDatabaseQuerier::getRandomCard() const +{ + if (db->cards.isEmpty()) + return {}; + + const auto keys = db->cards.keys(); + int randomIndex = QRandomGenerator::global()->bounded(keys.size()); + const QString &randomKey = keys.at(randomIndex); + CardInfoPtr randomCard = getCardInfo(randomKey); + + return ExactCard{randomCard, getPreferredPrinting(randomCard)}; +} + +ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const +{ + // The source card does not have a printing defined, which means we can't get a card from the same set. + if (otherPrinting.isEmpty()) { + return getCard({cardName}); + } + + // The source card does have a printing defined, which means we can attempt to get a card from the same set. + PrintingInfo relatedPrinting = getSpecificPrinting(cardName, otherPrinting.getSet()->getCorrectedShortName(), ""); + ExactCard relatedCard(guessCard({cardName}).getCardPtr(), relatedPrinting); + + // If we didn't find a card from the same set, just try to find any card with the same name. + return relatedCard ? relatedCard : getCard({cardName}); +} + +/** + * Finds the PrintingInfo in the cardInfo that has the given uuid field. + * + * @param cardInfo The CardInfo to search + * @param providerId The uuid to look for + * @return The PrintingInfo, or a default-constructed PrintingInfo if not found. + */ +PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const +{ + for (const auto &printings : cardInfo->getSets()) { + for (const auto &printing : printings) { + if (printing.getUuid() == providerId) { + return printing; + } + } + } + + return PrintingInfo(); +} + +PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const CardRef &cardRef) const +{ + CardInfoPtr cardInfo = getCardInfo(cardRef.name); + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + return findPrintingWithId(cardInfo, cardRef.providerId); +} + +PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const QString &cardName, + const QString &setShortName, + const QString &collectorNumber) const +{ + CardInfoPtr cardInfo = getCardInfo(cardName); + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + SetToPrintingsMap setMap = cardInfo->getSets(); + if (setMap.empty()) { + return PrintingInfo(nullptr); + } + + for (const auto &printings : setMap) { + for (auto &cardInfoForSet : printings) { + if (!collectorNumber.isEmpty()) { + if (cardInfoForSet.getSet()->getShortName() == setShortName && + cardInfoForSet.getProperty("num") == collectorNumber) { + return cardInfoForSet; + } + } else { + if (cardInfoForSet.getSet()->getShortName() == setShortName) { + return cardInfoForSet; + } + } + } + } + + return PrintingInfo(nullptr); +} + +/** + * Gets the card representing the preferred printing of the cardInfo + * + * @param cardName The cardName to find the preferred card and printing for + * @return A specific printing of a card + */ +ExactCard CardDatabaseQuerier::getPreferredCard(const QString &cardName) const +{ + return getPreferredCard(getCardInfo(cardName)); +} + +/** + * Gets the card representing the preferred printing of the cardInfo + * + * @param cardInfo The cardInfo to find the preferred printing for + * @return A specific printing of a card + */ +ExactCard CardDatabaseQuerier::getPreferredCard(const CardInfoPtr &cardInfo) const +{ + return ExactCard(cardInfo, getPreferredPrinting(cardInfo)); +} + +bool CardDatabaseQuerier::isPreferredPrinting(const CardRef &cardRef) const +{ + if (cardRef.providerId.startsWith("card_")) { + return cardRef.providerId == + QLatin1String("card_") + cardRef.name + QString("_") + getPreferredPrintingProviderId(cardRef.name); + } + return cardRef.providerId == getPreferredPrintingProviderId(cardRef.name); +} + +PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const QString &cardName) const +{ + CardInfoPtr cardInfo = getCardInfo(cardName); + return getPreferredPrinting(cardInfo); +} + +PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardInfo) const +{ + if (!cardInfo) { + return PrintingInfo(nullptr); + } + + const auto &pinnedPrintingProviderId = prefs->getCardPreferenceOverride(cardInfo->getName()); + + if (!pinnedPrintingProviderId.isEmpty()) { + return getSpecificPrinting({cardInfo->getName(), pinnedPrintingProviderId}); + } + + SetToPrintingsMap setMap = cardInfo->getSets(); + if (setMap.empty()) { + return PrintingInfo(nullptr); + } + + CardSetPtr preferredSet = nullptr; + PrintingInfo preferredPrinting; + SetPriorityComparator comparator; + + for (const auto &printings : setMap) { + for (auto &printing : printings) { + CardSetPtr currentSet = printing.getSet(); + if (!preferredSet || comparator(currentSet, preferredSet)) { + preferredSet = currentSet; + preferredPrinting = printing; + } + } + } + + if (preferredSet) { + return preferredPrinting; + } + + return PrintingInfo(nullptr); +} + +QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardName) const +{ + PrintingInfo preferredPrinting = getPreferredPrinting(cardName); + QString uuid = preferredPrinting.getUuid(); + if (!uuid.isEmpty()) { + return uuid; + } + + CardInfoPtr defaultCardInfo = getCardInfo(cardName); + if (defaultCardInfo.isNull()) { + return cardName; + } + return defaultCardInfo->getName(); +} + +QStringList CardDatabaseQuerier::getAllMainCardTypes() const +{ + QSet types; + for (const auto &card : db->cards.values()) { + types.insert(card->getMainCardType()); + } + return types.values(); +} + +QMap CardDatabaseQuerier::getAllMainCardTypesWithCount() const +{ + QMap typeCounts; + + for (const auto &card : db->cards.values()) { + QString type = card->getMainCardType(); + typeCounts[type]++; + } + + return typeCounts; +} + +QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const +{ + QMap typeCounts; + + for (const auto &card : db->cards.values()) { + QString type = card->getCardType(); + + QStringList parts = type.split(" — "); + + if (parts.size() > 1) { // Ensure there are subtypes + QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); + + for (const QString &subtype : subtypes) { + typeCounts[subtype]++; + } + } + } + + return typeCounts; +} + +FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const +{ + return db->formats.value(formatName.toLower()); +} + +QMap CardDatabaseQuerier::getAllFormatsWithCount() const +{ + QMap formatCounts; + + for (const auto &card : db->cards.values()) { + QStringList allProps = card->getProperties(); + + for (const QString &prop : allProps) { + if (prop.startsWith("format-")) { + QString formatName = prop.mid(QStringLiteral("format-").size()); + formatCounts[formatName]++; + } + } + } + + return formatCounts; +} diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h new file mode 100644 index 000000000..ff8d7958b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h @@ -0,0 +1,225 @@ +#ifndef COCKATRICE_CARD_DATABASE_QUERIER_H +#define COCKATRICE_CARD_DATABASE_QUERIER_H + +#include "../card_info.h" +#include "../printing/exact_card.h" + +#include +#include +#include + +class CardDatabase; + +/** + * @class CardDatabaseQuerier + * @ingroup CardDatabase + * @brief Provides lookup and convenience functions for querying cards and their printings. + * + * The CardDatabaseQuerier class offers various lookup helpers for retrieving card information + * (e.g., CardInfoPtr, ExactCard, and PrintingInfo) from a CardDatabase. It also applies user + * printing preferences via ICardPreferenceProvider when determining preferred printings. + */ +class CardDatabaseQuerier : public QObject +{ + Q_OBJECT + +public: + /** + * @brief Constructs a CardDatabaseQuerier. + * + * @param parent Parent QObject. + * @param db Pointer to the CardDatabase used for lookups. + * @param prefs Pointer to card preference provider which supplies user-preference for printings. + */ + explicit CardDatabaseQuerier(QObject *parent, const CardDatabase *db, const ICardPreferenceProvider *prefs); + + /** + * @brief Retrieves a card by its exact name. + * + * @param cardName Exact card name. + * @return A CardInfoPtr, or null if no matching card exists. + */ + [[nodiscard]] CardInfoPtr getCardInfo(const QString &cardName) const; + + /** + * @brief Retrieves multiple cards by their exact names. + * + * Failed lookups are skipped and not included in the result. + * + * @param cardNames List of exact card names. + * @return List of CardInfoPtr objects for which a match was found. + */ + [[nodiscard]] QList getCardInfos(const QStringList &cardNames) const; + + /** + * @brief Retrieves a card using simplified name matching. + * + * The name is automatically normalized, so callers do not need to simplify it. + * + * @param cardName A (possibly simplified or misspelled) card name. + * @return A CardInfoPtr, or null if not found. + */ + [[nodiscard]] CardInfoPtr getCardBySimpleName(const QString &cardName) const; + + /** + * @brief Looks up a card using exact name first, then simplified matching as fallback. + * + * @param name Raw card name input. + * @return The best-match CardInfoPtr, or null if no match is found. + */ + [[nodiscard]] CardInfoPtr lookupCardByName(const QString &name) const; + + /** + * @brief Converts a CardRef into an ExactCard. + * + * If the providerId is empty, the preferred printing is used. + * If providerId exists but cannot be found, an ExactCard with an empty PrintingInfo is returned. + * + * @param cardRef Card reference with name and optional providerId. + * @return The resolved ExactCard, or empty if no card was found. + */ + [[nodiscard]] ExactCard getCard(const CardRef &cardRef) const; + + /** + * @brief Resolves multiple CardRefs into ExactCards. + * + * Failed entries are not included in the result. + * + * @param cardRefs List of card references. + * @return List of successfully resolved ExactCards. + */ + [[nodiscard]] QList getCards(const QList &cardRefs) const; + + /** + * @brief Attempts a more flexible card lookup using both simple name matching and CardRef rules. + * + * If providerId is missing, uses preferred printing. If lookup fails, attempts simplified name. + * + * @param cardRef Card reference to resolve. + * @return The best-guess ExactCard, or empty if unresolved. + */ + [[nodiscard]] ExactCard guessCard(const CardRef &cardRef) const; + + /** + * @brief Returns a random card from the database using the preferred printing. + * + * @return A random ExactCard, or empty if the database is empty. + */ + [[nodiscard]] ExactCard getRandomCard() const; + + /** + * @brief Returns a printing of a card from the same set as another given printing when possible. + * + * If no matching printing exists, falls back to a standard lookup. + * + * @param cardName Card to retrieve. + * @param otherPrinting Printing to match the set against. + * @return Matching ExactCard if found, otherwise fallback ExactCard. + */ + [[nodiscard]] ExactCard getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const; + + /** + * @brief Returns the preferred printing of a card based on user preferences and set priority. + * + * @param cardName Name of the card. + * @return The preferred ExactCard. + */ + [[nodiscard]] ExactCard getPreferredCard(const QString &cardName) const; + + /** + * @brief Returns the preferred printing of a card based on user preferences and set priority. + * + * @param cardInfo Card information object. + * @return The preferred ExactCard. + */ + [[nodiscard]] ExactCard getPreferredCard(const CardInfoPtr &cardInfo) const; + + /** + * @brief Checks whether the CardRef refers to the preferred printing. + * + * @param cardRef Card reference to test. + * @return True if providerId matches the preferred printing. + */ + [[nodiscard]] bool isPreferredPrinting(const CardRef &cardRef) const; + + /** + * @brief Returns the preferred printing for the given card name. + * + * @param cardName Card name. + * @return Preferred PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo getPreferredPrinting(const QString &cardName) const; + + /** + * @brief Returns the preferred printing for the given card. + * + * @param cardInfo Card information object. + * @return Preferred PrintingInfo, or empty if not applicable. + */ + [[nodiscard]] PrintingInfo getPreferredPrinting(const CardInfoPtr &cardInfo) const; + + /** + * @brief Returns the providerId of the preferred printing. + * + * @param cardName Card name. + * @return ProviderId string for preferred printing. + */ + [[nodiscard]] QString getPreferredPrintingProviderId(const QString &cardName) const; + + /** + * @brief Retrieves a specific printing referenced by CardRef. + * + * @param cardRef Card reference including providerId. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo getSpecificPrinting(const CardRef &cardRef) const; + + /** + * @brief Searches for a specific printing by set code and collector number. + * + * @param cardName Card name to search. + * @param setCode Set (short) code to match. + * @param collectorNumber Collector number. If empty, any printing from the set is returned. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo + getSpecificPrinting(const QString &cardName, const QString &setCode, const QString &collectorNumber) const; + + /** + * @brief Searches for a printing that matches a given providerId. + * + * @param card Card to search. + * @param providerId Provider identifier to match. + * @return Matching PrintingInfo, or empty if not found. + */ + [[nodiscard]] PrintingInfo findPrintingWithId(const CardInfoPtr &card, const QString &providerId) const; + + /** + * @brief Returns a list of all main card types present in the database. + * + * @return List of main card type strings. + */ + [[nodiscard]] QStringList getAllMainCardTypes() const; + + /** + * @brief Returns a mapping of main card types to their occurrence counts. + * + * @return Map of main card type to count. + */ + [[nodiscard]] QMap getAllMainCardTypesWithCount() const; + + /** + * @brief Returns a mapping of card subtypes to their occurrence counts. + * + * @return Map of subtype string to count. + */ + [[nodiscard]] QMap getAllSubCardTypesWithCount() const; + FormatRulesPtr getFormat(const QString &formatName) const; + QMap getAllFormatsWithCount() const; + +private: + const CardDatabase *db; //!< Card database used for all lookups. + const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings. +}; + +#endif // COCKATRICE_CARD_DATABASE_QUERIER_H diff --git a/cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp similarity index 71% rename from cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp rename to libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp index ac1372f7f..f7e6bbfcf 100644 --- a/cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.cpp @@ -1,7 +1,13 @@ #include "card_database_parser.h" +#include + SetNameMap ICardDatabaseParser::sets; +ICardDatabaseParser::ICardDatabaseParser(ICardSetPriorityController *_cardSetPriorityController) + : cardSetPriorityController(_cardSetPriorityController) +{ +} void ICardDatabaseParser::clearSetlist() { sets.clear(); @@ -17,7 +23,7 @@ CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName, return sets.value(setName); } - CardSetPtr newSet = CardSet::newInstance(setName); + CardSetPtr newSet = CardSet::newInstance(cardSetPriorityController, setName); newSet->setLongName(longName); newSet->setSetType(setType); newSet->setReleaseDate(releaseDate); diff --git a/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h new file mode 100644 index 000000000..a8eceab5a --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h @@ -0,0 +1,92 @@ +#ifndef CARDDATABASE_PARSER_H +#define CARDDATABASE_PARSER_H + +#include "../../card_info.h" + +#include +#include + +#define COCKATRICE_XML_XSI_NAMESPACE "http://www.w3.org/2001/XMLSchema-instance" + +/** + * @class ICardDatabaseParser + * @ingroup CardDatabase + * @brief Defines the base parser interface (ICardDatabaseParser) for all card database parsers. + * + * Provides methods for checking file compatibility, parsing, and saving card databases. + * Also provides shared access to the global set list for cross-referencing. + */ +class ICardDatabaseParser : public QObject +{ + Q_OBJECT +public: + ICardDatabaseParser(ICardSetPriorityController *cardSetPriorityController); + ~ICardDatabaseParser() override = default; + + /** + * @brief Checks whether this parser can parse the given file. + * @param name File name (used for extension checks). + * @param device QIODevice representing the file content. + * @return true if the parser can handle this file. + */ + virtual bool getCanParseFile(const QString &name, QIODevice &device) = 0; + + /** + * @brief Parses a database file and emits addCard/addSet signals. + * @param device QIODevice representing the file content. + */ + virtual void parseFile(QIODevice &device) = 0; + + /** + * @brief Saves card and set data to a file. + * @param _formats + * @param sets Map of sets to save. + * @param cards Map of cards to save. + * @param fileName Target file path. + * @param sourceUrl Optional source URL of the database. + * @param sourceVersion Optional version string of the source. + * @return true if save succeeded. + */ + virtual bool saveToFile(FormatRulesNameMap _formats, + SetNameMap sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") = 0; + + /** @brief Clears the cached global set list. */ + static void clearSetlist(); + +protected: + /** @brief Cached global list of sets shared between all parsers. */ + static SetNameMap sets; + ICardSetPriorityController *cardSetPriorityController; + + /** + * @brief Internal helper to add a set to the global set cache. + * @param setName Short set name. + * @param longName Optional full name. + * @param setType Optional set type string. + * @param releaseDate Optional release date. + * @param priority Optional priority (fallback if not specified). + * @return Pointer to the added or existing CardSet instance. + */ + CardSetPtr internalAddSet(const QString &setName, + const QString &longName = "", + const QString &setType = "", + const QDate &releaseDate = QDate(), + const CardSet::Priority priority = CardSet::PriorityFallback); + +signals: + /** Emitted when a card is loaded from the database. */ + void addCard(CardInfoPtr card); + + /** Emitted when a set is loaded from the database. */ + void addSet(CardSetPtr set); + + void addFormat(FormatRulesPtr format); +}; + +Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser") + +#endif diff --git a/cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp similarity index 93% rename from cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp rename to libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 1f69079cd..ba27d63c4 100644 --- a/cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -1,5 +1,8 @@ #include "cockatrice_xml_3.h" +#include "../../relation/card_relation.h" +#include "../../relation/card_relation_type.h" + #include #include #include @@ -11,6 +14,11 @@ #define COCKATRICE_XML3_SCHEMALOCATION \ "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v3/cards.xsd" +CockatriceXml3Parser::CockatriceXml3Parser(ICardSetPriorityController *_cardSetPriorityController) + : ICardDatabaseParser(_cardSetPriorityController) +{ +} + bool CockatriceXml3Parser::getCanParseFile(const QString &fileName, QIODevice &device) { qCInfo(CockatriceXml3Log) << "Trying to parse: " << fileName; @@ -232,7 +240,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) _sets[setName].append(setInfo); // related cards } else if (xmlName == "related" || xmlName == "reverse-related") { - CardRelation::AttachType attach = CardRelation::DoesNotAttach; + CardRelationType attach = CardRelationType::DoesNotAttach; bool exclude = false; bool variable = false; int count = 1; @@ -254,7 +262,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) } if (attrs.hasAttribute("attach")) { - attach = CardRelation::AttachTo; + attach = CardRelationType::AttachTo; } if (attrs.hasAttribute("exclude")) { @@ -279,9 +287,13 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) } properties.insert("colors", colors); - CardInfoPtr newCard = - CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, _sets, cipt, - landscapeOrientation, tableRow, upsideDown); + + CardInfo::UiAttributes attributes = {.cipt = cipt, + .landscapeOrientation = landscapeOrientation, + .tableRow = tableRow, + .upsideDownArt = upsideDown}; + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); emit addCard(newCard); } } @@ -414,14 +426,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in } // positioning - xml.writeTextElement("tablerow", QString::number(info->getTableRow())); - if (info->getCipt()) { + const CardInfo::UiAttributes &attributes = info->getUiAttributes(); + xml.writeTextElement("tablerow", QString::number(attributes.tableRow)); + if (attributes.cipt) { xml.writeTextElement("cipt", "1"); } - if (info->getLandscapeOrientation()) { + if (attributes.landscapeOrientation) { xml.writeTextElement("landscapeOrientation", "1"); } - if (info->getUpsideDownArt()) { + if (attributes.upsideDownArt) { xml.writeTextElement("upsidedown", "1"); } @@ -430,12 +443,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in return xml; } -bool CockatriceXml3Parser::saveToFile(SetNameMap _sets, +bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, CardNameMap cards, const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) { + Q_UNUSED(_formats); + QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { return false; diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h new file mode 100644 index 000000000..a01b705aa --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h @@ -0,0 +1,79 @@ +#ifndef COCKATRICE_XML3_H +#define COCKATRICE_XML3_H + +#include "card_database_parser.h" + +#include +#include + +inline Q_LOGGING_CATEGORY(CockatriceXml3Log, "cockatrice_xml.xml_3_parser"); + +/** + * @class CockatriceXml3Parser + * @ingroup CardDatabase + * @brief Parses version 3 of the Cockatrice XML Schema. + * + * This parser reads a Cockatrice XML3 database and emits CardInfoPtr + * and CardSetPtr objects. All card properties are read individually. + * + * @note Differences from v4: + * - No block; properties are hardcoded (manacost, cmc, type, pt, loyalty, etc.). + * - No set priority field. + * - No support for rebalanced cards or preferences. + * - Related cards support only attach, exclude, variable, and count attributes. + */ +class CockatriceXml3Parser : public ICardDatabaseParser +{ + Q_OBJECT +public: + CockatriceXml3Parser(ICardSetPriorityController *cardSetPriorityController); + ~CockatriceXml3Parser() override = default; + + /** + * @brief Determines if the parser can handle this file. + * @param name File name. + * @param device Open QIODevice containing the XML. + * @return True if the file is a Cockatrice XML3 database. + */ + bool getCanParseFile(const QString &name, QIODevice &device) override; + + /** + * @brief Parse the XML database. + * @param device Open QIODevice positioned at start of file. + */ + void parseFile(QIODevice &device) override; + + /** + * @brief Save sets and cards back to an XML3 file. + */ + bool saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") override; + +private: + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses each node and emits addCard signals for each CardInfoPtr created. + */ + void loadCardsFromXml(QXmlStreamReader &xml); + + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses each node and adds them to the shared set cache. + */ + void loadSetsFromXml(QXmlStreamReader &xml); + + /** + * @brief Extracts the main card type from a full type string. + * @param type The full type string (e.g., "Legendary Artifact Creature - Golem") + * @return The primary type (e.g., "Creature"). + */ + QString getMainCardType(QString &type); +}; + +#endif \ No newline at end of file diff --git a/cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp similarity index 67% rename from cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp rename to libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index f9f6a98d4..cc0220526 100644 --- a/cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -1,11 +1,12 @@ #include "cockatrice_xml_4.h" -#include "../../../settings/cache_settings.h" +#include "../../relation/card_relation.h" #include #include #include #include +#include #include #define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase" @@ -13,6 +14,12 @@ #define COCKATRICE_XML4_SCHEMALOCATION \ "https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd" +CockatriceXml4Parser::CockatriceXml4Parser(ICardPreferenceProvider *_cardPreferenceProvider, + ICardSetPriorityController *_cardSetPriorityController) + : ICardDatabaseParser(_cardSetPriorityController), cardPreferenceProvider(_cardPreferenceProvider) +{ +} + bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device) { qCInfo(CockatriceXml4Log) << "Trying to parse: " << fileName; @@ -55,7 +62,9 @@ void CockatriceXml4Parser::parseFile(QIODevice &device) } auto xmlName = xml.name().toString(); - if (xmlName == "sets") { + if (xmlName == "formats") { + loadFormats(xml); + } else if (xmlName == "sets") { loadSetsFromXml(xml); } else if (xmlName == "cards") { loadCardsFromXml(xml); @@ -73,6 +82,116 @@ void CockatriceXml4Parser::parseFile(QIODevice &device) } } +static QSharedPointer parseFormat(QXmlStreamReader &xml) +{ + auto rulesPtr = FormatRulesPtr(new FormatRules()); + + if (xml.attributes().hasAttribute("formatName")) { + rulesPtr->formatName = xml.attributes().value("formatName").toString(); + } + + while (!xml.atEnd()) { + auto token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") { + break; + } + + if (token != QXmlStreamReader::StartElement) { + continue; + } + + QString xmlName = xml.name().toString(); + + if (xmlName == "minDeckSize") { + rulesPtr->minDeckSize = xml.readElementText().toInt(); + } else if (xmlName == "maxDeckSize") { + QString text = xml.readElementText(); + rulesPtr->maxDeckSize = text.toInt(); + } else if (xmlName == "maxSideboardSize") { + rulesPtr->maxSideboardSize = xml.readElementText().toInt(); + } else if (xmlName == "allowedCounts") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") { + + AllowedCount c; + + QString maxAttr = xml.attributes().value("max").toString(); + c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt(); + + c.label = xml.readElementText().trimmed(); + + rulesPtr->allowedCounts.append(c); + } + } + } else if (xmlName == "exceptions") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") { + ExceptionRule ex; + + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") { + break; + } + + if (token == QXmlStreamReader::StartElement) { + QString ename = xml.name().toString(); + + if (ename == "maxCopies") { + QString text = xml.readElementText(); + ex.maxCopies = (text == "unlimited") ? -1 : text.toInt(); + } else if (ename == "cardCondition") { + CardCondition cond; + cond.field = xml.attributes().value("field").toString(); + cond.matchType = xml.attributes().value("match").toString(); + cond.value = xml.attributes().value("value").toString(); + ex.conditions.append(cond); + xml.skipCurrentElement(); + } else { + xml.skipCurrentElement(); + } + } + } + + rulesPtr->exceptions.append(ex); + } + } + } else { + xml.skipCurrentElement(); + } + } + + return rulesPtr; +} + +void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name().toString() == "format") { + auto rulesPtr = parseFormat(xml); + emit addFormat(rulesPtr); + } + } +} + void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) { while (!xml.atEnd()) { @@ -131,7 +250,7 @@ QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &x void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) { - bool includeRebalancedCards = SettingsCache::instance().getIncludeRebalancedCards(); + bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards(); while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::EndElement) { break; @@ -205,7 +324,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } // related cards } else if (xmlName == "related" || xmlName == "reverse-related") { - CardRelation::AttachType attachType = CardRelation::DoesNotAttach; + CardRelationType attachType = CardRelationType::DoesNotAttach; bool exclude = false; bool variable = false; bool persistent = false; @@ -228,8 +347,8 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } if (attrs.hasAttribute("attach")) { - attachType = attrs.value("attach").toString() == "transform" ? CardRelation::TransformInto - : CardRelation::AttachTo; + attachType = attrs.value("attach").toString() == "transform" ? CardRelationType::TransformInto + : CardRelationType::AttachTo; } if (attrs.hasAttribute("exclude")) { @@ -257,14 +376,70 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) continue; } - CardInfoPtr newCard = - CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, _sets, cipt, - landscapeOrientation, tableRow, upsideDown); + CardInfo::UiAttributes attributes = {.cipt = cipt, + .landscapeOrientation = landscapeOrientation, + .tableRow = tableRow, + .upsideDownArt = upsideDown}; + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, _sets, attributes); emit addCard(newCard); } } } +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer &rulesPtr) +{ + if (rulesPtr.isNull()) { + qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr"; + return xml; + } + + const FormatRules &rules = *rulesPtr; + + xml.writeStartElement("format"); + if (!rules.formatName.isEmpty()) { + xml.writeAttribute("formatName", rules.formatName); + } + + xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize)); + xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0"); + xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize)); + if (!rules.allowedCounts.isEmpty()) { + xml.writeStartElement("allowedCounts"); + + for (const AllowedCount &c : rules.allowedCounts) { + xml.writeStartElement("count"); + xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max)); + xml.writeCharacters(c.label); + xml.writeEndElement(); // count + } + + xml.writeEndElement(); // allowedCounts + } + + if (!rules.exceptions.isEmpty()) { + xml.writeStartElement("exceptions"); + for (const ExceptionRule &ex : rules.exceptions) { + xml.writeStartElement("exception"); + xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies)); + + for (const CardCondition &cond : ex.conditions) { + xml.writeStartElement("cardCondition"); + xml.writeAttribute("field", cond.field); + xml.writeAttribute("match", cond.matchType); + xml.writeAttribute("value", cond.value); + xml.writeEndElement(); // cardCondition + } + + xml.writeEndElement(); // exception + } + xml.writeEndElement(); // exceptions + } + + xml.writeEndElement(); // format + return xml; +} + static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set) { if (set.isNull()) { @@ -374,14 +549,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in } // positioning - xml.writeTextElement("tablerow", QString::number(info->getTableRow())); - if (info->getCipt()) { + const CardInfo::UiAttributes &attributes = info->getUiAttributes(); + xml.writeTextElement("tablerow", QString::number(attributes.tableRow)); + if (attributes.cipt) { xml.writeTextElement("cipt", "1"); } - if (info->getLandscapeOrientation()) { + if (attributes.landscapeOrientation) { xml.writeTextElement("landscapeOrientation", "1"); } - if (info->getUpsideDownArt()) { + if (attributes.upsideDownArt) { xml.writeTextElement("upsidedown", "1"); } @@ -390,7 +566,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in return xml; } -bool CockatriceXml4Parser::saveToFile(SetNameMap _sets, +bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, CardNameMap cards, const QString &fileName, const QString &sourceUrl, @@ -417,6 +594,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets, xml.writeTextElement("sourceVersion", sourceVersion); xml.writeEndElement(); + if (_formats.count() > 0) { + xml.writeStartElement("formats"); + for (FormatRulesPtr format : _formats) { + xml << format; + } + xml.writeEndElement(); + } + if (_sets.count() > 0) { xml.writeStartElement("sets"); for (CardSetPtr set : _sets) { diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h new file mode 100644 index 000000000..189e79535 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h @@ -0,0 +1,86 @@ +#ifndef COCKATRICE_XML4_H +#define COCKATRICE_XML4_H + +#include "card_database_parser.h" + +#include +#include +#include + +inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser"); + +/** + * @class CockatriceXml4Parser + * @ingroup CardDatabase + * @brief Parses version 4 of the Cockatrice XML Schema. + * + * This parser reads a Cockatrice XML4 database and emits CardInfoPtr + * and CardSetPtr objects. Card properties are read inside blocks, + * making the parser more extensible and schema-compliant. + * + * @note Differences from v3: + * - Card properties are stored in blocks as a QVariantHash. + * - Sets can include a element. + * - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards). + * - Related cards support persistent relations and multiple attach types (e.g., transform). + * - More robust serialization; easier to extend schema in the future. + */ +class CockatriceXml4Parser : public ICardDatabaseParser +{ + Q_OBJECT +public: + explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider, + ICardSetPriorityController *cardSetPriorityController); + ~CockatriceXml4Parser() override = default; + + /** + * @brief Determines if the parser can handle this file. + * @param name File name. + * @param device Open QIODevice containing the XML. + * @return True if the file is a Cockatrice XML4 database. + */ + bool getCanParseFile(const QString &name, QIODevice &device) override; + + /** + * @brief Parse the XML database. + * @param device Open QIODevice positioned at start of file. + */ + void parseFile(QIODevice &device) override; + + /** + * @brief Save sets and cards back to an XML4 file. + */ + bool saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, + CardNameMap cards, + const QString &fileName, + const QString &sourceUrl = "unknown", + const QString &sourceVersion = "unknown") override; + +private: + ICardPreferenceProvider *cardPreferenceProvider; ///< Interface to handle user preferences + + /** + * @brief Loads a generic block from a element. + * @param xml The open QXmlStreamReader positioned at a element. + * @return A QVariantHash mapping property names to values. + */ + QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml); + + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Honors the user's preference regarding rebalanced cards. + */ + void loadCardsFromXml(QXmlStreamReader &xml); + + void loadFormats(QXmlStreamReader &xml); + /** + * @brief Load all elements from the XML stream. + * @param xml The open QXmlStreamReader positioned at the element. + * Parses nodes including priority information. + */ + void loadSetsFromXml(QXmlStreamReader &xml); +}; + +#endif \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp new file mode 100644 index 000000000..a6b3bb038 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp @@ -0,0 +1,53 @@ +#include "format_legality_rules.h" + +#include + +bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond) +{ + CardMatchType type = matchTypeFromString(cond.matchType); + QString fieldValue; + if (cond.field == "name") { + fieldValue = card.getName(); + } else if (cond.field == "text") { + fieldValue = card.getText(); + } else { + fieldValue = card.getProperty(cond.field); + } + + switch (type) { + case CardMatchType::Equals: + return fieldValue == cond.value; + case CardMatchType::NotEquals: + return fieldValue != cond.value; + case CardMatchType::Contains: + return fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::NotContains: + return !fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::Regex: { + QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption); + return re.match(fieldValue).hasMatch(); + } + default: + return false; + } +} + +bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule) +{ + for (const CardCondition &cond : rule.conditions) { + if (!cardMatchesCondition(card, cond)) { + return false; // all conditions must match + } + } + return true; +} + +bool cardHasAnyException(const CardInfo &card, const FormatRules &format) +{ + for (const ExceptionRule &rule : format.exceptions) { + if (exceptionAppliesToCard(card, rule)) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h new file mode 100644 index 000000000..16f2359ab --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h @@ -0,0 +1,73 @@ +#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H +#define COCKATRICE_FORMAT_LEGALITY_RULES_H + +#include +#include +#include + +class CardInfo; +using CardInfoPtr = QSharedPointer; + +struct CardCondition +{ + QString field; // e.g. "type", "maintype", "text" + QString matchType; // "contains", "equals", "regex", "notContains", etc. + QString value; // e.g. "Basic Land" +}; + +struct AllowedCount +{ + int max = 0; // 4, 1, 0, or -1 for unlimited + QString label; // "legal", "restricted", "banned" +}; + +struct ExceptionRule +{ + QList conditions; // All must match + int maxCopies = -1; // -1 = unlimited +}; + +struct FormatRules +{ + QString formatName; + int minDeckSize = 60; + int maxDeckSize = -1; // -1 = unlimited + int maxSideboardSize = 15; + + QList allowedCounts; + + QList exceptions; // Cards allowed to break maxCopies +}; + +enum class CardMatchType +{ + Equals, + NotEquals, + Contains, + NotContains, + Regex +}; + +// convert string to enum +inline CardMatchType matchTypeFromString(const QString &str) +{ + if (str == "equals") + return CardMatchType::Equals; + if (str == "notEquals") + return CardMatchType::NotEquals; + if (str == "contains") + return CardMatchType::Contains; + if (str == "notContains") + return CardMatchType::NotContains; + if (str == "regex") + return CardMatchType::Regex; + return CardMatchType::Equals; // fallback default +} + +bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond); + +bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule); + +bool cardHasAnyException(const CardInfo &card, const FormatRules &format); + +#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H diff --git a/cockatrice/src/game/game_specific_terms.h b/libcockatrice_card/libcockatrice/card/game_specific_terms.h similarity index 93% rename from cockatrice/src/game/game_specific_terms.h rename to libcockatrice_card/libcockatrice/card/game_specific_terms.h index cbf3e3ef9..2931365ad 100644 --- a/cockatrice/src/game/game_specific_terms.h +++ b/libcockatrice_card/libcockatrice/card/game_specific_terms.h @@ -1,3 +1,9 @@ +/** + * @file game_specific_terms.h + * @ingroup Cards + * @brief TODO: Document this. + */ + #ifndef GAME_SPECIFIC_TERMS_H #define GAME_SPECIFIC_TERMS_H @@ -47,6 +53,6 @@ inline static const QString getNicePropertyName(QString key) return QCoreApplication::translate("Mtg", "Color Identity"); return key; } -}; // namespace Mtg +} // namespace Mtg #endif diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp new file mode 100644 index 000000000..91ebd6647 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp @@ -0,0 +1,66 @@ +#include "card_name_normalizer.h" + +#include "../database/card_database_manager.h" +#include "../printing/exact_card.h" + +#include + +/** + * @brief Resolves the complete display name of a card. + * @param cardName Base name. + * @return Full display name, or the cardName unchanged if a display name is not found. + */ +static QString getCompleteCardName(const QString &cardName) +{ + ExactCard temp = CardDatabaseManager::query()->guessCard({cardName}); + if (temp) { + return temp.getName(); + } + + return cardName; +} + +QString CardNameNormalizer::operator()(const QString &cardNameString) const +{ + QString cardName = cardNameString; + + // Regex for advanced card parsing + static const QRegularExpression reSplitCard(R"( ?\/\/ ?)"); + static const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested + static const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string + static const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits + static const QRegularExpression reBraceDigit( + R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number + static const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)"); + + static const QHash differences{{QRegularExpression("’"), "'"}, + {QRegularExpression("Æ"), "Ae"}, + {QRegularExpression("æ"), "ae"}, + {QRegularExpression(" ?[|/]+ ?"), " // "}}; + + // Handle advanced card types + if (cardName.contains(reSplitCard)) { + cardName = cardName.split(reSplitCard).join(" // "); + } + + if (cardName.contains(reDoubleFacedMarker)) { + QStringList faces = cardName.split(reDoubleFacedMarker); + cardName = faces.first().trimmed(); + } + + // Remove unnecessary characters + cardName.remove(reBrace); + cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards + cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end + cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after + + // Normalize characters + for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { + cardName.replace(diff.key(), diff.value()); + } + + // Resolve complete card name + cardName = getCompleteCardName(cardName); + + return cardName; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h new file mode 100644 index 000000000..716e7da80 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H +#define COCKATRICE_CARD_NAME_NORMALIZER_H + +#include + +/** + * Functor that normalizes the raw card name parsed during a plaintext deck import into the card name that Cockatrice + * uses. + */ +struct CardNameNormalizer +{ + QString operator()(const QString &cardNameString) const; +}; + +#endif // COCKATRICE_CARD_NAME_NORMALIZER_H diff --git a/cockatrice/src/game/cards/exact_card.cpp b/libcockatrice_card/libcockatrice/card/printing/exact_card.cpp similarity index 96% rename from cockatrice/src/game/cards/exact_card.cpp rename to libcockatrice_card/libcockatrice/card/printing/exact_card.cpp index fbd60071e..993b5b96e 100644 --- a/cockatrice/src/game/cards/exact_card.cpp +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.cpp @@ -1,5 +1,8 @@ #include "exact_card.h" +#include "../card_info.h" +#include "printing_info.h" + /** * Default constructor. * This will set the CardInfoPtr to null. diff --git a/libcockatrice_card/libcockatrice/card/printing/exact_card.h b/libcockatrice_card/libcockatrice/card/printing/exact_card.h new file mode 100644 index 000000000..74fc75da3 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/exact_card.h @@ -0,0 +1,119 @@ +#ifndef EXACT_CARD_H +#define EXACT_CARD_H + +#include "../card_info.h" + +/** + * @class ExactCard + * @ingroup CardPrintings + * + * @brief Represents a specific card instance, defined by its CardInfo + * and a particular printing. + * + * An ExactCard identifies a card not only by its underlying CardInfoPtr + * (which may be null), but also by its PrintingInfo, which specifies the + * exact printing/variant. This allows distinguishing between different + * printings of the same logical card (e.g., different sets, promos, foils). + */ +class ExactCard +{ + CardInfoPtr card; + PrintingInfo printing; + +public: + /** + * @brief Constructs an empty ExactCard. + * + * The CardInfoPtr will be null, and PrintingInfo will be default-constructed. + * An empty ExactCard represents "no card". + */ + ExactCard(); + + /** + * @brief Constructs an ExactCard from a card and printing. + * + * @param _card The card info pointer. May be null. + * @param _printing The printing details. Defaults to an empty PrintingInfo. + */ + explicit ExactCard(const CardInfoPtr &_card, const PrintingInfo &_printing = PrintingInfo()); + + /** + * @brief Returns the underlying CardInfoPtr. + * + * May be null if the ExactCard is empty. + */ + [[nodiscard]] CardInfoPtr getCardPtr() const + { + return card; + } + + /** + * @brief Returns the printing information associated with this card. + * + * May be empty if no specific printing was assigned. + */ + [[nodiscard]] PrintingInfo getPrinting() const + { + return printing; + } + + /** + * @brief Compares both card pointer and printing for equality. + * + * Two ExactCard objects are equal only if both their CardInfoPtr and + * PrintingInfo values are equal. + */ + bool operator==(const ExactCard &other) const; + + /** + * @brief Convenience helper to get the card's display name. + * + * @return The card's name, or an empty string if the CardInfoPtr is null. + */ + [[nodiscard]] QString getName() const; + + /** + * @brief Returns a reference to the underlying CardInfo object. + * + * If the CardInfoPtr is null, returns a reference to a static empty CardInfo + * instance instead. This avoids null-dereferencing but means modifications + * to the returned object do not affect the ExactCard. + * + * @return A const reference to the CardInfo object. + */ + [[nodiscard]] const CardInfo &getInfo() const; + + /** + * @brief Generates a stable cache key for pixmap caching. + * + * The key includes the card's name and (if present) the printing UUID, + * allowing different printings of the same card to map to different cache entries. + */ + [[nodiscard]] QString getPixmapCacheKey() const; + + /** + * @brief Indicates whether this ExactCard represents no valid card. + * + * An ExactCard is considered empty if the CardInfoPtr is null or the + * card's name is empty. + */ + [[nodiscard]] bool isEmpty() const; + + /** + * @brief Boolean conversion indicating whether the card is valid (non-empty). + * + * @return true if not empty, false otherwise. + */ + explicit operator bool() const; + + /** + * @brief Emits the pixmapUpdated signal on the underlying CardInfo. + * + * Assumes CardInfoPtr is non-null. If called on an empty ExactCard, + * the behavior is undefined. + */ + void emitPixmapUpdated() const; +}; +Q_DECLARE_METATYPE(ExactCard) + +#endif // EXACT_CARD_H diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp new file mode 100644 index 000000000..340998565 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.cpp @@ -0,0 +1,20 @@ +#include "printing_info.h" + +#include "../set/card_set.h" + +PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set) +{ +} + +/** + * Gets the uuid property of the printing, or an empty string if the property isn't present + */ +QString PrintingInfo::getUuid() const +{ + return properties.value("uuid").toString(); +} + +QString PrintingInfo::getFlavorName() const +{ + return properties.value("flavorName").toString(); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/printing/printing_info.h b/libcockatrice_card/libcockatrice/card/printing/printing_info.h new file mode 100644 index 000000000..ad7b33654 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/printing/printing_info.h @@ -0,0 +1,131 @@ +#ifndef COCKATRICE_PRINTING_INFO_H +#define COCKATRICE_PRINTING_INFO_H + +#include "../set/card_set.h" + +#include +#include +#include + +class PrintingInfo; + +using SetToPrintingsMap = QMap>; + +/** + * @class PrintingInfo + * @ingroup CardPrintings + * + * @brief Represents metadata for a specific variation of a card within a set. + * + * A card can have multiple variations across sets. PrintingInfo associates + * a card with one such variation, and provides per-printing attributes + * such as identifiers or additional properties. + * + * Equality is defined as both the set and the property values being equal. + */ +class PrintingInfo +{ +public: + /** + * @brief Constructs a PrintingInfo associated with a specific set. + * + * @param _set The set this printing belongs to (defaults to null). + */ + explicit PrintingInfo(const CardSetPtr &_set = nullptr); + + /** + * @brief Destroys the PrintingInfo. + * + * Defaulted since no special cleanup is required. + */ + ~PrintingInfo() = default; + + /** + * @brief Equality operator. + * + * Two PrintingInfo objects are equal if they refer to the same set + * and contain the exact same property key/value pairs. + * + * @param other Another PrintingInfo to compare against. + * @return True if both set and properties are equal, otherwise false. + */ + bool operator==(const PrintingInfo &other) const + { + return this->set == other.set && this->properties == other.properties; + } + + /** + * @brief check if the info is empty, as if default constructed. + * + * @return True if both set and properties are empty, otherwise false. + */ + bool isEmpty() const + { + return set == nullptr && properties.isEmpty(); + } + +private: + CardSetPtr set; ///< The set this variation belongs to. + QVariantHash properties; ///< Key-value store for variation-specific attributes. + +public: + /** + * @brief Returns the set this printing belongs to. + * + * @return Pointer to the associated CardSet. + */ + [[nodiscard]] CardSetPtr getSet() const + { + return set; + } + + /** + * @brief Returns the list of property names defined for this printing. + * + * @return List of keys stored in the properties map. + */ + [[nodiscard]] QStringList getProperties() const + { + return properties.keys(); + } + + /** + * @brief Retrieves the value of a specific property. + * + * @param propertyName The key name of the property to query. + * @return The property value as a string, or an empty string if not set. + */ + [[nodiscard]] QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + + /** + * @brief Sets or updates the value of a specific property. + * + * If the property already exists, its value is replaced. + * + * @param _name The name of the property. + * @param _value The string value to assign. + */ + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); + } + + /** + * @brief Returns the providerID for this printing. + * + * @return A string representing the providerID. + */ + [[nodiscard]] QString getUuid() const; + + /** + * @brief Returns the flavorName for this printing. + * + * @return The flavorName, or empty if it isn't present. + */ + [[nodiscard]] QString getFlavorName() const; +}; + +#endif // COCKATRICE_PRINTING_INFO_H diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp new file mode 100644 index 000000000..90e59e439 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp @@ -0,0 +1,14 @@ +#include "card_relation.h" + +#include "card_relation_type.h" + +CardRelation::CardRelation(const QString &_name, + CardRelationType _attachType, + bool _isCreateAllExclusion, + bool _isVariableCount, + int _defaultCount, + bool _isPersistent) + : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), + isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) +{ +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.h b/libcockatrice_card/libcockatrice/card/relation/card_relation.h new file mode 100644 index 000000000..9ff704097 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.h @@ -0,0 +1,156 @@ +#ifndef COCKATRICE_CARD_RELATION_H +#define COCKATRICE_CARD_RELATION_H + +#include "card_relation_type.h" + +#include +#include + +/** + * @class CardRelation + * @ingroup Cards + * + * @brief Represents a relationship between two cards. + * + * CardRelation objects define directional relationships, such as: + * - One card attaching to another. + * - One card transforming into another. + * - One card creating another instance. + * + * Relations may also define metadata such as whether multiple creations + * are possible, whether the relation is persistent, and default counts. + */ +class CardRelation : public QObject +{ + Q_OBJECT + +private: + QString name; ///< Name of the related card. + CardRelationType attachType; ///< Type of attachment. + bool isCreateAllExclusion; ///< True if this relation should exclude multiple creations in "create all" operations. + bool isVariableCount; ///< True if the number of creations is variable. + int defaultCount; ///< Default number of cards created or involved. + bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. + +public: + /** + * @brief Constructs a CardRelation with optional parameters. + * + * @param _name Name of the related card. + * @param _attachType Type of attachment. + * @param _isCreateAllExclusion Whether this relation excludes mass creation. + * @param _isVariableCount Whether the count is variable. + * @param _defaultCount Default number for creations or transformations. + * @param _isPersistent Whether the relation persists across zone changes. + */ + explicit CardRelation(const QString &_name = QString(), + CardRelationType _attachType = CardRelationType::DoesNotAttach, + bool _isCreateAllExclusion = false, + bool _isVariableCount = false, + int _defaultCount = 1, + bool _isPersistent = false); + + /** + * @brief Returns the name of the related card. + * + * @return Name as QString reference. + */ + [[nodiscard]] inline const QString &getName() const + { + return name; + } + + /** + * @brief Returns the type of attachment. + * + * @return Enum value representing the attachment type. + */ + [[nodiscard]] CardRelationType getAttachType() const + { + return attachType; + } + + /** + * @brief Returns true if the card is attached to another. + * + * @return True if attached, false otherwise. + */ + [[nodiscard]] bool getDoesAttach() const + { + return attachType != CardRelationType::DoesNotAttach; + } + + /** + * @brief Returns true if this card transforms into another card. + * + * @return True if it transforms, false otherwise. + */ + [[nodiscard]] bool getDoesTransform() const + { + return attachType == CardRelationType::TransformInto; + } + + /** + * @brief Returns a string description of the attachment type. + * + * @return "attach" for AttachTo, "transform" for TransformInto, empty string otherwise. + */ + [[nodiscard]] QString getAttachTypeAsString() const + { + return cardAttachTypeToString(attachType); + } + + /** + * @brief Determines whether another instance can be created. + * + * @return True if creation is allowed, false if constrained by attachment. + */ + [[nodiscard]] bool getCanCreateAnother() const + { + return !getDoesAttach(); + } + + /** + * @brief Returns whether this relation is excluded from "create all" operations. + * + * @return True if excluded, false otherwise. + */ + [[nodiscard]] bool getIsCreateAllExclusion() const + { + return isCreateAllExclusion; + } + + /** + * @brief Returns whether the relation count is variable. + * + * @return True if variable, false otherwise. + */ + [[nodiscard]] bool getIsVariable() const + { + return isVariableCount; + } + + /** + * @brief Returns the default count of related cards. + * + * @return Integer representing default number. + */ + [[nodiscard]] int getDefaultCount() const + { + return defaultCount; + } + + /** + * @brief Returns whether the relation is persistent. + * + * Persistent relations are not destroyed on zone changes. + * + * @return True if persistent, false otherwise. + */ + [[nodiscard]] bool getIsPersistent() const + { + return isPersistent; + } +}; + +#endif // COCKATRICE_CARD_RELATION_H diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h b/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h new file mode 100644 index 000000000..9bd85f3ac --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation_type.h @@ -0,0 +1,35 @@ +#ifndef COCKATRICE_CARD_RELATION_TYPE_H +#define COCKATRICE_CARD_RELATION_TYPE_H + +#include + +/** + * @enum CardRelationType + * @ingroup Cards + * @brief Types of attachments between cards. + * + * DoesNotAttach: No attachment is present. + * AttachTo: This card attaches to another card. + * TransformInto: This card transforms into another card. + */ +enum class CardRelationType +{ + DoesNotAttach = 0, + AttachTo = 1, + TransformInto = 2, +}; + +// Helper function to transform the enum values into human-readable strings +inline QString cardAttachTypeToString(CardRelationType type) +{ + switch (type) { + case CardRelationType::AttachTo: + return "attach"; + case CardRelationType::TransformInto: + return "transform"; + default: + return ""; + } +} + +#endif // COCKATRICE_CARD_RELATION_TYPE_H diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.cpp b/libcockatrice_card/libcockatrice/card/set/card_set.cpp new file mode 100644 index 000000000..20d0aced8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set.cpp @@ -0,0 +1,85 @@ +#include "card_set.h" + +#include +#include + +const char *CardSet::TOKENS_SETNAME = "TK"; + +CardSet::CardSet(ICardSetPriorityController *_priorityController, + const QString &_shortName, + const QString &_longName, + const QString &_setType, + const QDate &_releaseDate, + const CardSet::Priority _priority) + : priorityController(std::move(_priorityController)), shortName(_shortName), longName(_longName), + releaseDate(_releaseDate), setType(_setType), priority(_priority) +{ + loadSetOptions(); +} + +CardSetPtr CardSet::newInstance(ICardSetPriorityController *_priorityController, + const QString &_shortName, + const QString &_longName, + const QString &_setType, + const QDate &_releaseDate, + const Priority _priority) +{ + CardSetPtr ptr(new CardSet(_priorityController, _shortName, _longName, _setType, _releaseDate, _priority)); + // ptr->setSmartPointer(ptr); + return ptr; +} + +QString CardSet::getCorrectedShortName() const +{ + // For Windows machines. + QSet invalidFileNames; + invalidFileNames << "CON" + << "PRN" + << "AUX" + << "NUL" + << "COM1" + << "COM2" + << "COM3" + << "COM4" + << "COM5" + << "COM6" + << "COM7" + << "COM8" + << "COM9" + << "LPT1" + << "LPT2" + << "LPT3" + << "LPT4" + << "LPT5" + << "LPT6" + << "LPT7" + << "LPT8" + << "LPT9"; + + return invalidFileNames.contains(shortName) ? shortName + "_" : shortName; +} + +void CardSet::loadSetOptions() +{ + sortKey = priorityController->getSortKey(shortName); + enabled = priorityController->isEnabled(shortName); + isknown = priorityController->isKnown(shortName); +} + +void CardSet::setSortKey(unsigned int _sortKey) +{ + sortKey = _sortKey; + priorityController->setSortKey(shortName, _sortKey); +} + +void CardSet::setEnabled(bool _enabled) +{ + enabled = _enabled; + priorityController->setEnabled(shortName, _enabled); +} + +void CardSet::setIsKnown(bool _isknown) +{ + isknown = _isknown; + priorityController->setIsKnown(shortName, _isknown); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/set/card_set.h b/libcockatrice_card/libcockatrice/card/set/card_set.h new file mode 100644 index 000000000..fe4b66522 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set.h @@ -0,0 +1,231 @@ +#ifndef COCKATRICE_CARD_SET_H +#define COCKATRICE_CARD_SET_H + +#include +#include +#include +#include +#include + +class CardInfo; +using CardInfoPtr = QSharedPointer; + +class CardSet; +using CardSetPtr = QSharedPointer; + +/** + * @class CardSet + * @ingroup CardSets + * + * @brief A collection of cards grouped under a common identifier. + * + * A set serves both as metadata (identifier, title, category, release date, and priority) + * and as a container of all cards that belong to it. Each set can be enabled/disabled + * and marked as known/unknown depending on context. + * + * The class inherits from `QList`, so it can be iterated over directly + * to access its contents. + * + * Typical usage: + * - Query metadata such as identifier, category, or release date. + * - Enable or disable sets according to user preference. + * - Store and retrieve CardInfo objects associated with the set. + */ +class CardSet : public QList +{ +public: + /** + * @enum Priority + * @brief Defines relative ordering and importance of sets. + */ + enum Priority + { + PriorityFallback = 0, ///< Used when no other priority is defined. + PriorityPrimary = 10, ///< Primary, canonical set. + PrioritySecondary = 20, ///< Secondary but relevant. + PriorityReprint = 30, ///< Duplicate or reprint category. + PriorityOther = 40, ///< Miscellaneous grouping. + PriorityLowest = 100, ///< Lowest sorting priority. + }; + + static const char *TOKENS_SETNAME; ///< Reserved identifier for token-like sets. + +private: + ICardSetPriorityController *priorityController; ///< Interface to the card set priority controller. + QString shortName; ///< Short identifier for the set. + QString longName; ///< Full name for the set. + unsigned int sortKey; ///< Custom numeric sort key. + QDate releaseDate; ///< Release date, may be empty if unknown. + QString setType; ///< Type/category label for the set. + Priority priority; ///< Priority level for sorting and relevance. + bool enabled; ///< Whether the set is active/enabled. + bool isknown; ///< Whether the set is considered known. + +public: + /** + * @brief Constructs a CardSet. + * + * @param priorityController Interface to a card set priority controller. + * @param _shortName Identifier string. + * @param _longName Full descriptive name. + * @param _setType Type/category string. + * @param _releaseDate Release date (optional). + * @param _priority Sorting/priority level. + */ + explicit CardSet(ICardSetPriorityController *priorityController, + const QString &_shortName = QString(), + const QString &_longName = QString(), + const QString &_setType = QString(), + const QDate &_releaseDate = QDate(), + const Priority _priority = PriorityFallback); + + /** + * @brief Creates and returns a new shared CardSet instance. + * + * @param priorityController Interface to a card set priority controller. + * @param _shortName Identifier string. + * @param _longName Full descriptive name. + * @param _setType Type/category string. + * @param _releaseDate Release date (optional). + * @param _priority Sorting/priority level. + * @return A shared pointer to the new CardSet. + */ + static CardSetPtr newInstance(ICardSetPriorityController *priorityController, + const QString &_shortName = QString(), + const QString &_longName = QString(), + const QString &_setType = QString(), + const QDate &_releaseDate = QDate(), + const Priority _priority = PriorityFallback); + + /** + * @brief Returns a safe, sanitized version of the short name. + * + * Intended for file paths or identifiers where only certain + * characters are allowed. + * + * @return Sanitized short name. + */ + [[nodiscard]] QString getCorrectedShortName() const; + + /// @return Short identifier of the set. + [[nodiscard]] QString getShortName() const + { + return shortName; + } + + /// @return Descriptive name of the set. + [[nodiscard]] QString getLongName() const + { + return longName; + } + + /// @return Type/category string of the set. + [[nodiscard]] QString getSetType() const + { + return setType; + } + + /// @return Release date of the set. + [[nodiscard]] QDate getReleaseDate() const + { + return releaseDate; + } + + /// @return Priority level of the set. + [[nodiscard]] Priority getPriority() const + { + return priority; + } + + /** + * @brief Sets the full name of the set. + * @param _longName New full name. + */ + void setLongName(const QString &_longName) + { + longName = _longName; + } + + /** + * @brief Sets the category/type of the set. + * @param _setType New category string. + */ + void setSetType(const QString &_setType) + { + setType = _setType; + } + + /** + * @brief Sets the release date of the set. + * @param _releaseDate New release date. + */ + void setReleaseDate(const QDate &_releaseDate) + { + releaseDate = _releaseDate; + } + + /** + * @brief Updates the priority of the set. + * @param _priority New priority value. + */ + + void setPriority(const Priority _priority) + { + priority = _priority; + } + + /** + * @brief Loads state values (enabled, known, sort key) from configuration. + * + * Reads external configuration and applies it to this set. + */ + void loadSetOptions(); + + /// @return The sort key assigned to this set. + [[nodiscard]] int getSortKey() const + { + return sortKey; + } + + /** + * @brief Assigns a new sort key to this set. + * @param _sortKey The numeric key to use for sorting. + */ + void setSortKey(unsigned int _sortKey); + + /// @return True if the set is enabled. + [[nodiscard]] bool getEnabled() const + { + return enabled; + } + + /** + * @brief Enables or disables the set. + * @param _enabled True to enable, false to disable. + */ + void setEnabled(bool _enabled); + + /// @return True if the set is considered known. + [[nodiscard]] bool getIsKnown() const + { + return isknown; + } + + /** + * @brief Marks the set as known or unknown. + * @param _isknown True if known, false if unknown. + */ + void setIsKnown(bool _isknown); + + /** + * @brief Determines whether the set has incomplete metadata and should be ignored. + * + * @return True if the long name, type, and release date are all empty. + */ + [[nodiscard]] bool getIsKnownIgnored() const + { + return longName.length() + setType.length() + releaseDate.toString().length() == 0; + } +}; + +#endif // COCKATRICE_CARD_SET_H diff --git a/cockatrice/src/utility/card_set_comparator.h b/libcockatrice_card/libcockatrice/card/set/card_set_comparator.h similarity index 91% rename from cockatrice/src/utility/card_set_comparator.h rename to libcockatrice_card/libcockatrice/card/set/card_set_comparator.h index bca3084e1..96f1052a9 100644 --- a/cockatrice/src/utility/card_set_comparator.h +++ b/libcockatrice_card/libcockatrice/card/set/card_set_comparator.h @@ -1,7 +1,13 @@ +/** + * @file card_set_comparator.h + * @ingroup CardSets + * @brief TODO: Document this. + */ + #ifndef SET_PRIORITY_COMPARATOR_H #define SET_PRIORITY_COMPARATOR_H -#include "../game/cards/card_info.h" +#include "../card_info.h" class SetPriorityComparator { diff --git a/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp b/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp new file mode 100644 index 000000000..8d6aa7365 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set_list.cpp @@ -0,0 +1,127 @@ +#include "card_set_list.h" + +class CardSetList::KeyCompareFunctor +{ +public: + inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const + { + if (a.isNull() || b.isNull()) { + // qCWarning(CardInfoLog) << "SetList::KeyCompareFunctor a or b is null"; + return false; + } + + return a->getSortKey() < b->getSortKey(); + } +}; + +void CardSetList::sortByKey() +{ + std::sort(begin(), end(), KeyCompareFunctor()); +} + +int CardSetList::getEnabledSetsNum() +{ + int num = 0; + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && set->getEnabled()) { + ++num; + } + } + return num; +} + +int CardSetList::getUnknownSetsNum() +{ + int num = 0; + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + ++num; + } + } + return num; +} + +QStringList CardSetList::getUnknownSetsNames() +{ + QStringList sets = QStringList(); + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + sets << set->getShortName(); + } + } + return sets; +} + +void CardSetList::enableAllUnknown() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + set->setIsKnown(true); + set->setEnabled(true); + } else if (set && set->getIsKnownIgnored() && !set->getEnabled()) { + set->setEnabled(true); + } + } +} + +void CardSetList::enableAll() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + + if (set == nullptr) { + // qCWarning(CardInfoLog) << "enabledAll has null"; + continue; + } + + if (!set->getIsKnownIgnored()) { + set->setIsKnown(true); + } + + set->setEnabled(true); + } +} + +void CardSetList::markAllAsKnown() +{ + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) { + set->setIsKnown(true); + set->setEnabled(false); + } else if (set && set->getIsKnownIgnored() && !set->getEnabled()) { + set->setEnabled(true); + } + } +} + +void CardSetList::guessSortKeys() +{ + defaultSort(); + for (int i = 0; i < size(); ++i) { + CardSetPtr set = at(i); + if (set.isNull()) { + // qCWarning(CardInfoLog) << "guessSortKeys set is null"; + continue; + } + set->setSortKey(i); + } +} + +void CardSetList::defaultSort() +{ + std::sort(begin(), end(), [](const CardSetPtr &a, const CardSetPtr &b) { + // Sort by priority, then by release date, then by short name + if (a->getPriority() != b->getPriority()) { + return a->getPriority() < b->getPriority(); // lowest first + } else if (a->getReleaseDate() != b->getReleaseDate()) { + return a->getReleaseDate() > b->getReleaseDate(); // most recent first + } else { + return a->getShortName() < b->getShortName(); // alphabetically + } + }); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/set/card_set_list.h b/libcockatrice_card/libcockatrice/card/set/card_set_list.h new file mode 100644 index 000000000..81d605374 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/set/card_set_list.h @@ -0,0 +1,102 @@ +#ifndef COCKATRICE_CARD_SET_LIST_H +#define COCKATRICE_CARD_SET_LIST_H + +#include "card_set.h" + +#include + +/** + * @class CardSetList + * @ingroup CardSets + * + * @brief A list-like container for CardSet objects with extended management methods. + * + * Extends `QList` by adding convenience operations for sorting, + * enabling/disabling sets, and tracking known/unknown status. Unlike a plain + * list, this container provides domain-specific functionality for handling + * groups of sets in bulk. + */ +class CardSetList : public QList +{ +private: + /** + * @class KeyCompareFunctor + * @brief Internal comparison functor for sorting by sort key. + * + * Used internally by `sortByKey()` to order sets consistently + * according to their assigned numeric sort keys. + */ + class KeyCompareFunctor; + +public: + /** + * @brief Sorts the set list by each set’s assigned sort key. + * + * Uses KeyCompareFunctor internally. If two sets share the + * same sort key, their relative order is unspecified. + */ + void sortByKey(); + + /** + * @brief Reassigns sort keys based on the current order. + * + * Calls defaultSort() and then assigns sequential sort keys + * to all sets according to their resulting positions, replacing + * any existing sort keys to ensure consistent ordering. + */ + void guessSortKeys(); + + /** + * @brief Enables all sets that are unknown or ignored. + * + * Sets that are not marked as known and not ignored are marked as known + * and enabled. Ignored-known sets are also enabled, but remain ignored. + */ + void enableAllUnknown(); + + /** + * @brief Enables all sets in the list. + * + * Equivalent to calling `setEnabled(true)` on each entry. + */ + void enableAll(); + + /** + * @brief Marks all sets as known and adjusts their enabled state. + * + * Unknown, non-ignored sets become known and disabled. + * Ignored-known sets are enabled if they were previously disabled. + */ + void markAllAsKnown(); + + /** + * @brief Counts the number of sets that are currently enabled. + * + * @return Integer count of enabled sets. + */ + int getEnabledSetsNum(); + + /** + * @brief Counts the number of sets that are currently unknown. + * + * @return Integer count of unknown sets. + */ + int getUnknownSetsNum(); + + /** + * @brief Collects the short names of all sets marked as unknown. + * + * @return A list of unknown set names. + */ + QStringList getUnknownSetsNames(); + + /** + * @brief Sorts the list by default rules. + * + * Orders sets first by priority (ascending), then by release date + * (most recent first), and finally alphabetically by short name. + */ + void defaultSort(); +}; + +#endif // COCKATRICE_CARD_SET_LIST_H diff --git a/libcockatrice_deck_list/CMakeLists.txt b/libcockatrice_deck_list/CMakeLists.txt new file mode 100644 index 000000000..5ccdb5f66 --- /dev/null +++ b/libcockatrice_deck_list/CMakeLists.txt @@ -0,0 +1,40 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/deck_list/tree/abstract_deck_list_card_node.h + libcockatrice/deck_list/tree/abstract_deck_list_node.h + libcockatrice/deck_list/tree/deck_list_card_node.h + libcockatrice/deck_list/tree/inner_deck_list_node.h + libcockatrice/deck_list/deck_list.h + libcockatrice/deck_list/deck_list_history_manager.h + libcockatrice/deck_list/deck_list_node_tree.h + libcockatrice/deck_list/deck_list_memento.h + libcockatrice/deck_list/sideboard_plan.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_deck_list STATIC + ${MOC_SOURCES} + libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp + libcockatrice/deck_list/tree/abstract_deck_list_node.cpp + libcockatrice/deck_list/tree/deck_list_card_node.cpp + libcockatrice/deck_list/tree/inner_deck_list_node.cpp + libcockatrice/deck_list/deck_list.cpp + libcockatrice/deck_list/deck_list_history_manager.cpp + libcockatrice/deck_list/deck_list_node_tree.cpp + libcockatrice/deck_list/sideboard_plan.cpp +) + +add_dependencies(libcockatrice_deck_list libcockatrice_protocol) + +target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE}) diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp new file mode 100644 index 000000000..e3e7b41c0 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -0,0 +1,512 @@ +#include "deck_list.h" + +#include "deck_list_memento.h" +#include "tree/abstract_deck_list_node.h" +#include "tree/deck_list_card_node.h" +#include "tree/inner_deck_list_node.h" + +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < 0x050600 +// qHash on QRegularExpression was added in 5.6, FIX IT +uint qHash(const QRegularExpression &key, uint seed) noexcept +{ + return qHash(key.pattern(), seed); // call qHash on pattern QString instead +} +#endif + +static const QString CURRENT_SIDEBOARD_PLAN_KEY = ""; + +bool DeckList::Metadata::isEmpty() const +{ + return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty(); +} + +DeckList::DeckList() +{ +} + +DeckList::DeckList(const QString &nativeString) +{ + loadFromString_Native(nativeString); +} + +DeckList::DeckList(const Metadata &metadata, + const DecklistNodeTree &tree, + const QMap &sideboardPlans) + : metadata(metadata), sideboardPlans(sideboardPlans), tree(tree) +{ +} + +QList DeckList::getCurrentSideboardPlan() const +{ + if (!sideboardPlans.contains(CURRENT_SIDEBOARD_PLAN_KEY)) { + return {}; + } + + return sideboardPlans.value(CURRENT_SIDEBOARD_PLAN_KEY).getMoveList(); +} + +void DeckList::setCurrentSideboardPlan(const QList &plan) +{ + sideboardPlans[CURRENT_SIDEBOARD_PLAN_KEY].setMoveList(plan); +} + +bool DeckList::readElement(QXmlStreamReader *xml) +{ + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "lastLoadedTimestamp") { + metadata.lastLoadedTimestamp = xml->readElementText(); + } else if (childName == "deckname") { + metadata.name = xml->readElementText(); + } else if (childName == "format") { + metadata.gameFormat = xml->readElementText(); + } else if (childName == "comments") { + metadata.comments = xml->readElementText(); + } else if (childName == "bannerCard") { + QString providerId = xml->attributes().value("providerId").toString(); + QString cardName = xml->readElementText(); + metadata.bannerCard = {cardName, providerId}; + } else if (childName == "tags") { + metadata.tags.clear(); // Clear existing tags + while (xml->readNextStartElement()) { + if (xml->name().toString() == "tag") { + metadata.tags.append(xml->readElementText()); + } + } + } else if (childName == "zone") { + tree.readZoneElement(xml); + } else if (childName == "sideboard_plan") { + SideboardPlan newSideboardPlan; + if (newSideboardPlan.readElement(xml)) { + sideboardPlans.insert(newSideboardPlan.getName(), newSideboardPlan); + } + } + } else if (xml->isEndElement() && (childName == "cockatrice_deck")) { + return false; + } + return true; +} + +static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata) +{ + xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); + xml->writeTextElement("deckname", metadata.name); + xml->writeTextElement("format", metadata.gameFormat); + xml->writeStartElement("bannerCard"); + xml->writeAttribute("providerId", metadata.bannerCard.providerId); + xml->writeCharacters(metadata.bannerCard.name); + xml->writeEndElement(); + xml->writeTextElement("comments", metadata.comments); + + // Write tags + xml->writeStartElement("tags"); + for (const QString &tag : metadata.tags) { + xml->writeTextElement("tag", tag); + } + xml->writeEndElement(); +} + +void DeckList::write(QXmlStreamWriter *xml) const +{ + xml->writeStartElement("cockatrice_deck"); + xml->writeAttribute("version", "1"); + + writeMetadata(xml, metadata); + + // Write zones + tree.write(xml); + + // Write sideboard plans + for (auto &sideboardPlan : sideboardPlans.values()) { + sideboardPlan.write(xml); + } + + xml->writeEndElement(); // Close "cockatrice_deck" +} + +bool DeckList::loadFromXml(QXmlStreamReader *xml) +{ + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } + + cleanList(); + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isStartElement()) { + if (xml->name().toString() != "cockatrice_deck") + return false; + while (!xml->atEnd()) { + xml->readNext(); + if (!readElement(xml)) + break; + } + } + } + refreshDeckHash(); + if (xml->error()) { + qDebug() << "Error loading deck from xml: " << xml->errorString(); + return false; + } + return true; +} + +bool DeckList::loadFromString_Native(const QString &nativeString) +{ + QXmlStreamReader xml(nativeString); + return loadFromXml(&xml); +} + +QString DeckList::writeToString_Native() const +{ + QString result; + QXmlStreamWriter xml(&result); + xml.writeStartDocument(); + write(&xml); + xml.writeEndDocument(); + return result; +} + +bool DeckList::loadFromFile_Native(QIODevice *device) +{ + QXmlStreamReader xml(device); + return loadFromXml(&xml); +} + +bool DeckList::saveToFile_Native(QIODevice *device) const +{ + QXmlStreamWriter xml(device); + xml.setAutoFormatting(true); + xml.writeStartDocument(); + + write(&xml); + + xml.writeEndDocument(); + return true; +} + +/** + * Clears the decklist and loads in a new deck from text + * + * @param in The text to load + * @param preserveMetadata If true, don't clear the existing metadata + * @param cardNameNormalizer Function that takes the parsed card name string in the text and + * @return False if the input was empty, true otherwise. + */ +bool DeckList::loadFromStream_Plain(QTextStream &in, + bool preserveMetadata, + const std::function &cardNameNormalizer) +{ + const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reEmpty("^\\s*$"); + const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b", + QRegularExpression::CaseInsensitiveOption); + + // Regex for advanced card parsing + const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); + + // Regex for extracting set code and collector number with attached symbols + const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); + const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); + + cleanList(preserveMetadata); + + auto inputs = in.readAll().trimmed().split('\n'); + auto max_line = inputs.size(); + + // Start at the first empty line before the first card line + auto deckStart = inputs.indexOf(reCardLine); + if (deckStart == -1) { + if (inputs.indexOf(reComment) == -1) { + return false; // Input is empty + } + deckStart = max_line; + } else { + deckStart = inputs.lastIndexOf(reEmpty, deckStart); + if (deckStart == -1) { + deckStart = 0; + } + } + + // find sideboard position, if marks are used this won't be needed + int sBStart = -1; + if (inputs.indexOf(reSBMark, deckStart) == -1) { + sBStart = inputs.indexOf(reSBComment, deckStart); + if (sBStart == -1) { + sBStart = inputs.indexOf(reEmpty, deckStart + 1); + if (sBStart == -1) { + sBStart = max_line; + } + auto nextCard = inputs.indexOf(reCardLine, sBStart + 1); + if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { + sBStart = max_line; + } + } + } + + int index = 0; + QRegularExpressionMatch match; + + // Parse name and comments + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.name = match.captured(); + break; + } + } + while (index < deckStart) { + const auto ¤t = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + metadata.comments += match.captured() + '\n'; + } + } + metadata.comments.chop(1); + + // 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) { + // check if line is a card + match = reCardLine.match(inputs.at(index)); + if (!match.hasMatch()) + continue; + + QString cardName = match.captured().simplified(); + bool sideboard = false; + + // Sideboard detection + if (sBStart < 0) { + match = reSBMark.match(cardName); + if (match.hasMatch()) { + sideboard = true; + cardName = match.captured(1); + } + } else { + if (index == sBStart) + continue; + sideboard = index > sBStart; + } + + // Extract set code, collector number, and foil + QString setCode; + QString collectorNumber; + bool isFoil = false; + + // Check for foil status at the end of the card name + if (cardName.endsWith("*F*", Qt::CaseInsensitive)) { + isFoil = true; + cardName.chop(3); // Remove the "*F*" from the card name + } + Q_UNUSED(isFoil); + + // Attempt to match the hyphen-separated format (PLST-2094) + match = reHyphenFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(2).toUpper(); + collectorNumber = match.captured(3); + cardName = cardName.left(match.capturedStart()).trimmed(); + } else { + // Attempt to match the regular format (PLST) 2094 + match = reRegularFormat.match(cardName); + if (match.hasMatch()) { + setCode = match.captured(1).toUpper(); + collectorNumber = match.captured(2); + cardName = cardName.left(match.capturedStart()).trimmed(); + } + } + + // check if a specific amount is mentioned + int amount = 1; + match = reMultiplier.match(cardName); + if (match.hasMatch()) { + amount = match.captured(1).toInt(); + cardName = match.captured(2); + } + + // Normalize the card name + cardName = cardNameNormalizer(cardName); + + // Determine the zone (mainboard/sideboard) + QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; + + // make new entry in decklist + tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber); + } + + refreshDeckHash(); + return true; +} + +bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) +{ + QTextStream in(device); + return loadFromStream_Plain(in, false, cardNameNormalizer); +} + +bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + auto writeToStream = [&stream, prefixSideboardCards, slashTappedOutSplitCards](const auto node, const auto card) { + if (prefixSideboardCards && node->getName() == DECK_ZONE_SIDE) { + stream << "SB: "; + } + if (!slashTappedOutSplitCards) { + stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName()); + } else { + stream << QString("%1 %2\n").arg(card->getNumber()).arg(card->getName().replace("//", "/")); + } + }; + + forEachCard(writeToStream); + return true; +} + +bool DeckList::saveToFile_Plain(QIODevice *device, bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + QTextStream out(device); + return saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards); +} + +QString DeckList::writeToString_Plain(bool prefixSideboardCards, bool slashTappedOutSplitCards) const +{ + QString result; + QTextStream out(&result); + saveToStream_Plain(out, prefixSideboardCards, slashTappedOutSplitCards); + return result; +} + +/** + * Clears all cards and other data from the decklist + * + * @param preserveMetadata If true, only clear the cards + */ +void DeckList::cleanList(bool preserveMetadata) +{ + tree.clear(); + if (!preserveMetadata) { + metadata = {}; + } + refreshDeckHash(); +} + +QStringList DeckList::getCardList(const QSet &restrictToZones) const +{ + auto nodes = tree.getCardNodes(restrictToZones); + + QStringList result; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), [](auto node) { return node->getName(); }); + + return result; +} + +QList DeckList::getCardRefList(const QSet &restrictToZones) const +{ + auto nodes = tree.getCardNodes(restrictToZones); + + QList result; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(result), + [](auto node) { return node->toCardRef(); }); + + return result; +} + +QList DeckList::getCardNodes(const QSet &restrictToZones) const +{ + return tree.getCardNodes(restrictToZones); +} + +QList DeckList::getZoneNodes(const QSet &restrictToZones) const +{ + return tree.getZoneNodes(restrictToZones); +} + +int DeckList::getSideboardSize() const +{ + auto cards = tree.getCardNodes({DECK_ZONE_SIDE}); + + int size = 0; + for (auto card : cards) { + size += card->getNumber(); + } + + return size; +} + +DecklistCardNode *DeckList::addCard(const QString &cardName, + const QString &zoneName, + int position, + const QString &cardSetName, + const QString &cardSetCollectorNumber, + const QString &cardProviderId, + bool formatLegal) +{ + auto node = + tree.addCard(cardName, 1, zoneName, position, cardSetName, cardSetCollectorNumber, cardProviderId, formatLegal); + refreshDeckHash(); + return node; +} + +/** + * Gets the deck hash. + * The hash is computed on the first call to this method, and is cached until the decklist is modified. + * + * @return The deck hash + */ +QString DeckList::getDeckHash() const +{ + if (!cachedDeckHash.isEmpty()) { + return cachedDeckHash; + } + + cachedDeckHash = tree.computeDeckHash(); + return cachedDeckHash; +} + +/** + * Invalidates the cached deckHash. + */ +void DeckList::refreshDeckHash() +{ + cachedDeckHash = QString(); +} + +/** + * Calls a given function on each card in the deck. + */ +void DeckList::forEachCard(const std::function &func) const +{ + tree.forEachCard(func); +} + +DeckListMemento DeckList::createMemento(const QString &reason) const +{ + return DeckListMemento(writeToString_Native(), reason); +} + +void DeckList::restoreMemento(const DeckListMemento &m) +{ + cleanList(); + loadFromString_Native(m.getMemento()); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h new file mode 100644 index 000000000..a96adeb38 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -0,0 +1,257 @@ +/** + * @file deck_list.h + * @brief Defines the DeckList class, which manages a full + * deck structure including cards, zones, sideboard plans, and + * serialization to/from multiple formats. This is a logic class which + * does not care about Qt or user facing views. + * See @c DeckListModel for the actual Qt Model to be used for views + */ + +#ifndef DECKLIST_H +#define DECKLIST_H + +#include "deck_list_memento.h" +#include "deck_list_node_tree.h" +#include "sideboard_plan.h" +#include "tree/inner_deck_list_node.h" + +#include +#include +#include +#include + +class AbstractDecklistNode; +class DecklistCardNode; +class CardDatabase; +class QIODevice; +class QTextStream; +class InnerDecklistNode; + +/** + * @class DeckList + * @ingroup Decks + * @brief Represents a complete deck, including metadata, zones, cards, + * and sideboard plans. + * + * A DeckList is a wrapper around an `InnerDecklistNode` tree, + * enriched with metadata like deck name, comments, tags, banner card, + * and multiple sideboard plans. + * + * ### Core responsibilities: + * - Store and manage the root node tree (zones → groups → cards). + * - Provide deck-level metadata (name, comments, tags, banner). + * - Support multiple sideboard plans (meta-game strategies). + * - Provide import/export in multiple formats: + * - Cockatrice native XML format. + * - Plain-text list format. + * - Provide hashing for deck identity (deck hash). + * + * ### Ownership: + * - Owns the `DecklistNodeTree`. + * - Owns `SideboardPlan` instances stored in `sideboardPlans`. + * + * ### Example workflow: + * ``` + * DeckList deck; + * deck.setName("Mono Red Aggro"); + * deck.addCard("Lightning Bolt", "main"); + * deck.addTag("Aggro"); + * deck.saveToFile_Native(device); + * ``` + */ +class DeckList +{ +public: + struct Metadata + { + QString name; ///< User-defined deck name. + QString comments; ///< Free-form comments or notes. + QString gameFormat; ///< The name of the game format this deck contains legal cards for + CardRef bannerCard; ///< Optional representative card for the deck. + QStringList tags; ///< User-defined tags for deck classification. + QString lastLoadedTimestamp; ///< Timestamp string of last load. + + /** + * @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty. + */ + bool isEmpty() const; + }; + +private: + Metadata metadata; ///< Deck metadata that is stored in the deck file + QMap sideboardPlans; ///< Named sideboard plans. + DecklistNodeTree tree; ///< The deck tree (zones + cards). + + /** + * @brief Cached deck hash, recalculated lazily. + * An empty string indicates the cache is invalid. + */ + mutable QString cachedDeckHash; + +public: + /// @name Metadata setters + ///@{ + void setName(const QString &_name = QString()) + { + metadata.name = _name; + } + void setComments(const QString &_comments = QString()) + { + metadata.comments = _comments; + } + void setTags(const QStringList &_tags = QStringList()) + { + metadata.tags = _tags; + } + void addTag(const QString &_tag) + { + metadata.tags.append(_tag); + } + void clearTags() + { + metadata.tags.clear(); + } + void setBannerCard(const CardRef &_bannerCard = {}) + { + metadata.bannerCard = _bannerCard; + } + void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString()) + { + metadata.lastLoadedTimestamp = _lastLoadedTimestamp; + } + void setGameFormat(const QString &_gameFormat = QString()) + { + metadata.gameFormat = _gameFormat; + } + ///@} + + /// @brief Construct an empty deck. + explicit DeckList(); + /// @brief Construct from a serialized native-format string. + explicit DeckList(const QString &nativeString); + /// @brief Construct from components + DeckList(const Metadata &metadata, + const DecklistNodeTree &tree, + const QMap &sideboardPlans = {}); + + /** + * @brief Gets a pointer to the underlying node tree. + * Note: DO NOT call this method unless the object needs to have access to the underlying model. + * For now, only the DeckListModel should be calling this. + */ + DecklistNodeTree *getTree() + { + return &tree; + } + + /// @name Metadata getters + /// The individual metadata getters still exist for backwards compatibility. + ///@{ + //! \todo Figure out when we can remove them. + const Metadata &getMetadata() const + { + return metadata; + } + QString getName() const + { + return metadata.name; + } + QString getComments() const + { + return metadata.comments; + } + QStringList getTags() const + { + return metadata.tags; + } + CardRef getBannerCard() const + { + return metadata.bannerCard; + } + QString getLastLoadedTimestamp() const + { + return metadata.lastLoadedTimestamp; + } + QString getGameFormat() const + { + return metadata.gameFormat; + } + ///@} + + bool isBlankDeck() const + { + return metadata.isEmpty() && getCardList().isEmpty(); + } + + /// @name Sideboard plans + ///@{ + QList getCurrentSideboardPlan() const; + void setCurrentSideboardPlan(const QList &plan); + const QMap &getSideboardPlans() const + { + return sideboardPlans; + } + ///@} + + /// @name Serialization (XML) + ///@{ + bool readElement(QXmlStreamReader *xml); + void write(QXmlStreamWriter *xml) const; + bool loadFromXml(QXmlStreamReader *xml); + bool loadFromString_Native(const QString &nativeString); + QString writeToString_Native() const; + bool loadFromFile_Native(QIODevice *device); + bool saveToFile_Native(QIODevice *device) const; + ///@} + + /// @name Serialization (Plain text) + ///@{ + bool loadFromStream_Plain(QTextStream &stream, + bool preserveMetadata, + const std::function &cardNameNormalizer); + bool loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer); + bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const; + bool + saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; + QString writeToString_Plain(bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; + ///@} + + /// @name Deck manipulation + ///@{ + void cleanList(bool preserveMetadata = false); + bool isEmpty() const + { + return tree.isEmpty() && metadata.isEmpty() && sideboardPlans.isEmpty(); + } + QStringList getCardList(const QSet &restrictToZones = {}) const; + QList getCardRefList(const QSet &restrictToZones = {}) const; + QList getCardNodes(const QSet &restrictToZones = {}) const; + QList getZoneNodes(const QSet &restrictToZones = {}) const; + int getSideboardSize() const; + + DecklistCardNode *addCard(const QString &cardName, + const QString &zoneName, + int position = -1, + const QString &cardSetName = QString(), + const QString &cardSetCollectorNumber = QString(), + const QString &cardProviderId = QString(), + const bool formatLegal = true); + ///@} + + /// @name Deck identity + ///@{ + QString getDeckHash() const; + void refreshDeckHash(); + ///@} + + /** + * @brief Apply a function to every card in the deck tree. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func) const; + DeckListMemento createMemento(const QString &reason) const; + void restoreMemento(const DeckListMemento &m); +}; + +#endif diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp new file mode 100644 index 000000000..83c9cc0bb --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.cpp @@ -0,0 +1,53 @@ +#include "deck_list_history_manager.h" + +void DeckListHistoryManager::save(const DeckListMemento &memento) +{ + undoStack.push(memento); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::clear() +{ + undoStack.clear(); + redoStack.clear(); + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::undo(DeckList *deck) +{ + if (undoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = undoStack.top(); + + // Save current state for redo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + redoStack.push(currentState); + + // Pop the last state from undo stack and restore it + DeckListMemento memento = undoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} + +void DeckListHistoryManager::redo(DeckList *deck) +{ + if (redoStack.isEmpty()) + return; + + // Peek at the memento we are going to restore + const DeckListMemento &mementoToRestore = redoStack.top(); + + // Save current state for undo + DeckListMemento currentState = deck->createMemento(mementoToRestore.getReason()); + undoStack.push(currentState); + + // Pop the next state from redo stack and restore it + DeckListMemento memento = redoStack.pop(); + deck->restoreMemento(memento); + + emit undoRedoStateChanged(); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h new file mode 100644 index 000000000..e6bd27e2d --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_history_manager.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_DECK_LIST_HISTORY_MANAGER_H +#define COCKATRICE_DECK_LIST_HISTORY_MANAGER_H + +#include "deck_list.h" +#include "deck_list_memento.h" + +#include +#include + +class DeckListHistoryManager : public QObject +{ + Q_OBJECT + +signals: + void undoRedoStateChanged(); + +public: + explicit DeckListHistoryManager(QObject *parent = nullptr) : QObject(parent) + { + } + + void save(const DeckListMemento &memento); + + void clear(); + + [[nodiscard]] bool canUndo() const + { + return !undoStack.isEmpty(); + } + + [[nodiscard]] bool canRedo() const + { + return !redoStack.isEmpty(); + } + + void undo(DeckList *deck); + + void redo(DeckList *deck); + + [[nodiscard]] QStack getRedoStack() const + { + return redoStack; + } + [[nodiscard]] QStack getUndoStack() const + { + return undoStack; + } + +private: + QStack undoStack; + QStack redoStack; +}; + +#endif // COCKATRICE_DECK_LIST_HISTORY_MANAGER_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h new file mode 100644 index 000000000..6ddf430c5 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_memento.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_DECK_LIST_MEMENTO_H +#define COCKATRICE_DECK_LIST_MEMENTO_H +#include + +class DeckListMemento +{ +public: + DeckListMemento() = default; + explicit DeckListMemento(const QString &memento, const QString &reason = QString()) + : memento(memento), reason(reason) + { + } + + [[nodiscard]] QString getMemento() const + { + return memento; + } + [[nodiscard]] QString getReason() const + { + return reason; + } + +private: + QString memento; + QString reason; +}; + +#endif // COCKATRICE_DECK_LIST_MEMENTO_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp new file mode 100644 index 000000000..644e0851a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.cpp @@ -0,0 +1,186 @@ +#include "deck_list_node_tree.h" + +#include "tree/deck_list_card_node.h" + +#include +#include + +DecklistNodeTree::DecklistNodeTree() : root(new InnerDecklistNode()) +{ +} + +DecklistNodeTree::DecklistNodeTree(const DecklistNodeTree &other) : root(new InnerDecklistNode(other.root)) +{ +} + +DecklistNodeTree &DecklistNodeTree::operator=(const DecklistNodeTree &other) +{ + if (this != &other) { + delete root; + root = new InnerDecklistNode(other.root); + } + return *this; +} + +DecklistNodeTree::~DecklistNodeTree() +{ + delete root; +} + +bool DecklistNodeTree::isEmpty() const +{ + return root->isEmpty(); +} + +void DecklistNodeTree::clear() +{ + root->clearTree(); +} + +QList DecklistNodeTree::getCardNodes(const QSet &restrictToZones) const +{ + QList result; + + for (auto *zoneNode : getZoneNodes(restrictToZones)) { + for (auto *cardNode : *zoneNode) { + auto *cardCardNode = dynamic_cast(cardNode); + if (cardCardNode) { + result.append(cardCardNode); + } + } + } + + return result; +} + +QList DecklistNodeTree::getZoneNodes(const QSet &restrictToZones) const +{ + QList zones; + for (auto *node : *root) { + InnerDecklistNode *currentZone = dynamic_cast(node); + if (!currentZone) + continue; + if (!restrictToZones.isEmpty() && !restrictToZones.contains(currentZone->getName())) { + continue; + } + zones.append(currentZone); + } + + return zones; +} + +QString DecklistNodeTree::computeDeckHash() const +{ + auto mainDeckNodes = getCardNodes({DECK_ZONE_MAIN}); + auto sideDeckNodes = getCardNodes({DECK_ZONE_SIDE}); + + static auto nodesToCardList = [](const QList &nodes, const QString &prefix = {}) { + QStringList result; + for (auto node : nodes) { + for (int i = 0; i < node->getNumber(); ++i) { + result.append(prefix + node->getName().toLower()); + } + } + return result; + }; + + QStringList cardList = nodesToCardList(mainDeckNodes) + nodesToCardList(sideDeckNodes, "SB:"); + + cardList.sort(); + QByteArray deckHashArray = QCryptographicHash::hash(cardList.join(";").toUtf8(), QCryptographicHash::Sha1); + quint64 number = (((quint64)(unsigned char)deckHashArray[0]) << 32) + + (((quint64)(unsigned char)deckHashArray[1]) << 24) + + (((quint64)(unsigned char)deckHashArray[2] << 16)) + + (((quint64)(unsigned char)deckHashArray[3]) << 8) + (quint64)(unsigned char)deckHashArray[4]; + return QString::number(number, 32).rightJustified(8, '0'); +} + +void DecklistNodeTree::write(QXmlStreamWriter *xml) const +{ + for (int i = 0; i < root->size(); i++) { + root->at(i)->writeElement(xml); + } +} + +void DecklistNodeTree::readZoneElement(QXmlStreamReader *xml) +{ + QString zoneName = xml->attributes().value("name").toString(); + InnerDecklistNode *newZone = getZoneObjFromName(zoneName); + newZone->readElement(xml); +} + +DecklistCardNode *DecklistNodeTree::addCard(const QString &cardName, + int amount, + const QString &zoneName, + int position, + const QString &cardSetName, + const QString &cardSetCollectorNumber, + const QString &cardProviderId, + const bool formatLegal) +{ + auto *zoneNode = getZoneObjFromName(zoneName); + auto *node = new DecklistCardNode(cardName, amount, zoneNode, position, cardSetName, cardSetCollectorNumber, + cardProviderId, formatLegal); + return node; +} + +bool DecklistNodeTree::deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode) +{ + if (node == root) { + return true; + } + + if (rootNode == nullptr) { + rootNode = root; + } + + int index = rootNode->indexOf(node); + if (index != -1) { + delete rootNode->takeAt(index); + + if (rootNode->empty()) { + deleteNode(rootNode, rootNode->getParent()); + } + + return true; + } + + for (int i = 0; i < rootNode->size(); i++) { + auto *inner = dynamic_cast(rootNode->at(i)); + if (inner) { + if (deleteNode(node, inner)) { + return true; + } + } + } + + return false; +} + +void DecklistNodeTree::forEachCard(const std::function &func) const +{ + // Support for this is only possible if the internal structure + // doesn't get more complicated. + for (int i = 0; i < root->size(); i++) { + InnerDecklistNode *node = dynamic_cast(root->at(i)); + for (int j = 0; j < node->size(); j++) { + DecklistCardNode *card = dynamic_cast(node->at(j)); + func(node, card); + } + } +} + +/** + * Gets the InnerDecklistNode that is the root node for the given zone, creating a new node if it doesn't exist. + */ +InnerDecklistNode *DecklistNodeTree::getZoneObjFromName(const QString &zoneName) const +{ + for (int i = 0; i < root->size(); i++) { + auto *node = dynamic_cast(root->at(i)); + if (node->getName() == zoneName) { + return node; + } + } + + return new InnerDecklistNode(zoneName, root); +} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h new file mode 100644 index 000000000..6de760634 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list_node_tree.h @@ -0,0 +1,92 @@ +#ifndef COCKATRICE_DECKLIST_NODE_TREE_H +#define COCKATRICE_DECKLIST_NODE_TREE_H + +#include "libcockatrice/utility/card_ref.h" +#include "tree/deck_list_card_node.h" +#include "tree/inner_deck_list_node.h" + +#include + +class DecklistNodeTree +{ + InnerDecklistNode *root; ///< Root of the deck tree (zones + cards). + +public: + /// @brief Constructs an empty DecklistNodeTree + explicit DecklistNodeTree(); + /// @brief Copy constructor. Deep copies the tree + explicit DecklistNodeTree(const DecklistNodeTree &other); + /// @brief Copy-assignment operator. Deep copies the tree + DecklistNodeTree &operator=(const DecklistNodeTree &other); + + virtual ~DecklistNodeTree(); + + /** + * @brief Gets a pointer to the underlying root node. + * Note: DO NOT call this method unless the object needs to have access to the underlying model. + * For now, only the DeckListModel should be calling this. + */ + InnerDecklistNode *getRoot() const + { + return root; + } + + bool isEmpty() const; + + /** + * @brief Deletes all nodes except the root. + */ + void clear(); + + /** + * Gets all card nodes in the tree + * @param restrictToZones Only get the nodes in these zones + * @return A QList containing all the card nodes in the zone. + */ + QList getCardNodes(const QSet &restrictToZones = {}) const; + + /** + * Gets all zone nodes in the tree + * @param restrictToZones If not empty, only get the zone nodes with these names. + * @return A QList containing all the zone nodes in the tree. + */ + QList getZoneNodes(const QSet &restrictToZones = {}) const; + + /** + * @brief Computes the deck hash + */ + QString computeDeckHash() const; + + /** + *@brief Writes the contents of the deck to xml + */ + void write(QXmlStreamWriter *xml) const; + + /** + * @brief Reads a "zone" section of the xml to this tree + */ + void readZoneElement(QXmlStreamReader *xml); + + DecklistCardNode *addCard(const QString &cardName, + int amount, + const QString &zoneName, + int position, + const QString &cardSetName = QString(), + const QString &cardSetCollectorNumber = QString(), + const QString &cardProviderId = QString(), + const bool formatLegal = true); + bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr); + + /** + * @brief Apply a function to every card in the deck tree. This can modify the cards. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func) const; + +private: + // Helpers for traversing the tree + InnerDecklistNode *getZoneObjFromName(const QString &zoneName) const; +}; + +#endif // COCKATRICE_DECKLIST_NODE_TREE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp new file mode 100644 index 000000000..d991ec98e --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.cpp @@ -0,0 +1,59 @@ +#include "sideboard_plan.h" + +#include + +SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) + : name(_name), moveList(_moveList) +{ +} + +void SideboardPlan::setMoveList(const QList &_moveList) +{ + moveList = _moveList; +} + +bool SideboardPlan::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "name") + name = xml->readElementText(); + else if (childName == "move_card_to_zone") { + MoveCard_ToZone m; + while (!xml->atEnd()) { + xml->readNext(); + const QString childName2 = xml->name().toString(); + if (xml->isStartElement()) { + if (childName2 == "card_name") + m.set_card_name(xml->readElementText().toStdString()); + else if (childName2 == "start_zone") + m.set_start_zone(xml->readElementText().toStdString()); + else if (childName2 == "target_zone") + m.set_target_zone(xml->readElementText().toStdString()); + } else if (xml->isEndElement() && (childName2 == "move_card_to_zone")) { + moveList.append(m); + break; + } + } + } + } else if (xml->isEndElement() && (childName == "sideboard_plan")) + return true; + } + return false; +} + +void SideboardPlan::write(QXmlStreamWriter *xml) const +{ + xml->writeStartElement("sideboard_plan"); + xml->writeTextElement("name", name); + for (auto &i : moveList) { + xml->writeStartElement("move_card_to_zone"); + xml->writeTextElement("card_name", QString::fromStdString(i.card_name())); + xml->writeTextElement("start_zone", QString::fromStdString(i.start_zone())); + xml->writeTextElement("target_zone", QString::fromStdString(i.target_zone())); + xml->writeEndElement(); + } + xml->writeEndElement(); +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h new file mode 100644 index 000000000..524217c2d --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/sideboard_plan.h @@ -0,0 +1,70 @@ +#ifndef COCKATRICE_SIDEBOARD_PLAN_H +#define COCKATRICE_SIDEBOARD_PLAN_H + +#include +#include + +class QXmlStreamWriter; +class QXmlStreamReader; + +/** + * @class SideboardPlan + * @ingroup Decks + * @brief Represents a predefined sideboarding strategy for a deck. + * + * Sideboard plans store a named list of card movements that should be applied + * between the mainboard and sideboard for a specific matchup. Each movement + * is expressed using a `MoveCard_ToZone` protobuf message. + * + * ### Responsibilities: + * - Store the plan name and list of moves. + * - Support XML serialization/deserialization. + * + * ### Typical usage: + * A deck can contain multiple sideboard plans (e.g., "vs Aggro", "vs Control"), + * each describing how to transform the main deck into its intended configuration. + */ +class SideboardPlan +{ +private: + QString name; ///< Human-readable name of this plan. + QList moveList; ///< List of move instructions for this plan. + +public: + /** + * @brief Construct a new SideboardPlan. + * @param _name The plan name. + * @param _moveList Initial list of card move instructions. + */ + explicit SideboardPlan(const QString &_name = "", const QList &_moveList = {}); + + /** + * @brief Read a SideboardPlan from an XML stream. + * @param xml XML reader positioned at the plan element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml); + + /** + * @brief Write this SideboardPlan to XML. + * @param xml Stream to append the serialized element to. + */ + void write(QXmlStreamWriter *xml) const; + + /// @return The plan name. + [[nodiscard]] QString getName() const + { + return name; + } + + /// @return Const reference to the move list. + [[nodiscard]] const QList &getMoveList() const + { + return moveList; + } + + /// @brief Replace the move list with a new one. + void setMoveList(const QList &_moveList); +}; + +#endif // COCKATRICE_SIDEBOARD_PLAN_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp new file mode 100644 index 000000000..b8a497c20 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.cpp @@ -0,0 +1,62 @@ +#include "abstract_deck_list_card_node.h" + +bool AbstractDecklistCardNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case ByNumber: + return compareNumber(other); + case ByName: + return compareName(other); + default: + return false; + } +} + +bool AbstractDecklistCardNode::compareNumber(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + int n1 = getNumber(); + int n2 = other2->getNumber(); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { + return true; + } +} + +bool AbstractDecklistCardNode::compareName(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return true; + } +} + +bool AbstractDecklistCardNode::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + if (xml->isEndElement() && xml->name().toString() == "card") + return false; + } + return true; +} + +void AbstractDecklistCardNode::writeElement(QXmlStreamWriter *xml) +{ + xml->writeEmptyElement("card"); + xml->writeAttribute("number", QString::number(getNumber())); + xml->writeAttribute("name", getName()); + + if (!getCardSetShortName().isEmpty()) { + xml->writeAttribute("setShortName", getCardSetShortName()); + } + if (!getCardCollectorNumber().isEmpty()) { + xml->writeAttribute("collectorNumber", getCardCollectorNumber()); + } + if (!getCardProviderId().isEmpty()) { + xml->writeAttribute("uuid", getCardProviderId()); + } +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h new file mode 100644 index 000000000..88d8b0930 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h @@ -0,0 +1,155 @@ +/** + * @file abstract_deck_list_card_node.h + * @brief Defines the AbstractDecklistCardNode base class, which adds + * card-specific behavior on top of AbstractDecklistNode. + * + * This class is the intermediate abstract base between the generic + * AbstractDecklistNode and concrete card entries such as DecklistCardNode + * or DecklistModelCardNode. + */ + +#ifndef COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H +#define COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H + +#include "abstract_deck_list_node.h" + +/** + * @class AbstractDecklistCardNode + * @ingroup DeckModels + * @brief Abstract base class for all deck list nodes that represent + * actual card entries. + * + * While AbstractDecklistNode provides the general interface for all + * nodes in the deck tree (zones, groups, cards), this subclass refines + * the interface to cover properties specific to *cards*: + * - Quantity (number of copies). + * - Name. + * - Set code and collector number. + * - Provider ID. + * + * ### Role in the hierarchy: + * - Leaf-oriented abstract class; no children of its own. + * - Serves as the base for concrete implementations: + * - @c DecklistCardNode: Stores real card data in the deck tree. + * - @c DecklistModelCardNode: Wraps a DecklistCardNode for use + * in the Qt model layer. + * + * ### Responsibilities: + * - Defines getters/setters for all card-identifying attributes. + * - Provides comparison logic for sorting by name or number. + * - Implements XML serialization for saving/loading deck files. + * + * ### Ownership: + * - As with all nodes, owned by its parent InnerDecklistNode. + */ +class AbstractDecklistCardNode : public AbstractDecklistNode +{ +public: + /** + * @brief Construct a new AbstractDecklistCardNode. + * + * @param _parent Optional parent node. If provided, this node + * will be inserted into the parent’s children list. + * @param position Index at which to insert into parent’s children. + * If -1, the node is appended to the end. + */ + explicit AbstractDecklistCardNode(InnerDecklistNode *_parent = nullptr, int position = -1) + : AbstractDecklistNode(_parent, position) + { + } + + /// @return The number of copies of this card in the deck. + [[nodiscard]] virtual int getNumber() const = 0; + + /// @param _number Set the number of copies of this card. + virtual void setNumber(int _number) = 0; + + /// @return The display name of this card. + [[nodiscard]] QString getName() const override = 0; + + /// @param _name Set the display name of this card. + virtual void setName(const QString &_name) = 0; + + /// @return The provider identifier for this card (e.g., UUID). + [[nodiscard]] virtual QString getCardProviderId() const override = 0; + + /// @param _cardProviderId Set the provider identifier for this card. + virtual void setCardProviderId(const QString &_cardProviderId) = 0; + + /// @return The abbreviated set code (e.g., "NEO"). + [[nodiscard]] virtual QString getCardSetShortName() const override = 0; + + /// @param _cardSetShortName Set the abbreviated set code. + virtual void setCardSetShortName(const QString &_cardSetShortName) = 0; + + /// @return The collector number of the card within its set. + [[nodiscard]] virtual QString getCardCollectorNumber() const override = 0; + + /// @param _cardSetNumber Set the collector number. + virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0; + + /// @return The format legality of the card + virtual bool getFormatLegality() const = 0; + + /// @param _formatLegal If the card is considered legal + virtual void setFormatLegality(const bool _formatLegal) = 0; + + /** + * @brief Get the height of this node in the tree. + * + * For card nodes, height is always 0 because they are leaf nodes + * and do not contain children. + * + * @return 0 + */ + [[nodiscard]] int height() const override + { + return 0; + } + + /** + * @brief Compare this card node against another for sorting. + * + * Uses the node’s current @c sortMethod to determine how to compare: + * - ByName: Alphabetical comparison. + * - ByNumber: Numerical comparison. + * - Default: Falls back to implementation-defined behavior. + * + * @param other Another node to compare against. + * @return true if this node should sort before @p other. + */ + bool compare(AbstractDecklistNode *other) const override; + + /** + * @brief Compare this card node to another by quantity. + * @param other Node to compare against. + * @return true if this node’s number < other’s number. + */ + bool compareNumber(AbstractDecklistNode *other) const; + + /** + * @brief Compare this card node to another by name. + * @param other Node to compare against. + * @return true if this node’s name comes before other’s name. + */ + bool compareName(AbstractDecklistNode *other) const; + + /** + * @brief Deserialize this node’s properties from XML. + * @param xml QXmlStreamReader positioned at the element. + * @return true if parsing succeeded. + * + * This supports loading deck files from Cockatrice’s XML format. + */ + bool readElement(QXmlStreamReader *xml) override; + + /** + * @brief Serialize this node’s properties to XML. + * @param xml Writer to append this node’s XML element. + * + * This supports saving deck files to Cockatrice’s XML format. + */ + void writeElement(QXmlStreamWriter *xml) override; +}; + +#endif // COCKATRICE_ABSTRACT_DECK_LIST_CARD_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp new file mode 100644 index 000000000..0f39ce71a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.cpp @@ -0,0 +1,24 @@ +#include "abstract_deck_list_node.h" + +#include "inner_deck_list_node.h" + +AbstractDecklistNode::AbstractDecklistNode(InnerDecklistNode *_parent, int position) + : parent(_parent), sortMethod(Default) +{ + if (parent) { + if (position == -1) { + parent->append(this); + } else { + parent->insert(position, this); + } + } +} + +int AbstractDecklistNode::depth() const +{ + if (parent) { + return parent->depth() + 1; + } else { + return 0; + } +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h new file mode 100644 index 000000000..877705705 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_node.h @@ -0,0 +1,185 @@ +/** + * @file abstract_deck_list_node.h + * @brief Defines the AbstractDecklistNode base class used as the foundation + * for all nodes in the deck list tree (zones, groups, and cards). + * + * The deck list is modeled as a tree: + * - The invisible root node is managed by DeckListModel. + * - Top-level children are zones (e.g. Mainboard, Sideboard). + * - Zones contain grouping nodes (e.g. by type, color, or mana cost). + * - Grouping nodes contain card nodes. + * + * This abstract base class provides the interface and shared functionality + * for all node types. Concrete subclasses (InnerDecklistNode, + * DecklistCardNode, DecklistModelCardNode, etc.) implement the specifics. + */ + +#ifndef COCKATRICE_ABSTRACT_DECK_LIST_NODE_H +#define COCKATRICE_ABSTRACT_DECK_LIST_NODE_H + +#include + +/** + * @enum DeckSortMethod + * @ingroup DeckModels + * @brief Defines the different sort strategies a node may use + * to order its children. + * + * Sorting behavior is typically set by the DeckListModel when the user + * requests sorting in the UI. + * + * - ByNumber: Sort numerically (often by collector number). + * - ByName: Sort alphabetically by card name. + * - Default: No explicit sorting; insertion order is preserved. + */ +enum DeckSortMethod +{ + ByNumber, ///< Sort by numeric properties (e.g. collector number). + ByName, ///< Sort by card name (locale-aware comparison). + Default ///< Leave in insertion order. +}; + +class InnerDecklistNode; + +/** + * @class AbstractDecklistNode + * @ingroup DeckModels + * @brief Base class for all nodes in the deck list tree. + * + * This class defines the common interface for every node in the + * deck representation: zones, groupings, and cards. + * + * Responsibilities: + * - Maintain a pointer to its parent (if any). + * - Track the sorting method to be used for child nodes. + * - Provide a consistent interface for retrieving basic identifying + * properties (name, set, collector number, provider ID). + * - Define abstract methods for XML serialization, used when saving + * or loading deck files. + * + * Lifetime / Ownership: + * - Nodes are arranged hierarchically under @c InnerDecklistNode parents. + * - The parent takes ownership of its children; destruction cascades. + * - The DeckListModel holds the invisible root node, which in turn + * owns the entire hierarchy. + * + * Extension: + * - @c InnerDecklistNode is the concrete subclass representing + * "folders" in the tree (zones, groups). + * - @c DecklistCardNode and @c DecklistModelCardNode represent + * actual card entries. + */ +class AbstractDecklistNode +{ +protected: + /** + * @brief Pointer to the parent node, or nullptr if this is the root. + * + * Ownership note: The parent is responsible for destroying this node + * when it is removed from the tree. + */ + InnerDecklistNode *parent; + + /** + * @brief Current sorting strategy for this node's children. + * + * Sorting is applied recursively by the DeckListModel when + * the view requests it. + */ + DeckSortMethod sortMethod; + +public: + /** + * @brief Construct a new AbstractDecklistNode and insert it into its parent. + * + * @param _parent Parent node. May be nullptr if this is the root. + * @param position Optional index at which to insert into the parent's + * children. If -1, the node is appended to the end. + * + * If a parent is provided, the constructor automatically appends + * or inserts this node into the parent’s child list. + */ + explicit AbstractDecklistNode(InnerDecklistNode *_parent = nullptr, int position = -1); + + /// Virtual destructor. Child classes must clean up their resources. + virtual ~AbstractDecklistNode() = default; + + /** + * @brief Set the sort method for this node’s children. + * @param method The sorting strategy to use. + * + * Subclasses may override if they need to apply additional logic. + */ + virtual void setSortMethod(DeckSortMethod method) + { + sortMethod = method; + } + + /** + * @name Core identification properties + * + * These methods provide a standard way for the model to retrieve + * identifying information about a node, regardless of type. + * @{ + */ + [[nodiscard]] virtual QString getName() const = 0; + [[nodiscard]] virtual QString getCardProviderId() const = 0; + [[nodiscard]] virtual QString getCardSetShortName() const = 0; + [[nodiscard]] virtual QString getCardCollectorNumber() const = 0; + /// @} + + /** + * @brief Whether this node is the "deck header" (deck metadata). + * + * This distinguishes special nodes that represent deck-level + * information rather than cards or groupings. + */ + [[nodiscard]] virtual bool isDeckHeader() const = 0; + + /// @return The parent node, or nullptr if this is the root. + [[nodiscard]] InnerDecklistNode *getParent() const + { + return parent; + } + + /** + * @brief Compute the depth of this node in the tree. + * @return Distance from the root (root = 0, children = 1, etc.). + */ + [[nodiscard]] int depth() const; + + /** + * @brief Compute the "height" of this node. + * + * Height is defined by subclasses; it usually represents how + * many levels of descendants this node spans. + * + * For example: + * - A card node has height 1. + * - A group node containing cards has height 2. + */ + [[nodiscard]] virtual int height() const = 0; + + /** + * @brief Compare this node against another for sorting. + * + * The semantics of comparison depend on the node type and the + * current @c sortMethod. + * + * @param other The node to compare against. + * @return true if this node should come before @p other. + */ + virtual bool compare(AbstractDecklistNode *other) const = 0; + + /** + * @name XML serialization + * These methods support reading and writing decks from/to + * Cockatrice deck XML format. + * @{ + */ + virtual bool readElement(QXmlStreamReader *xml) = 0; + virtual void writeElement(QXmlStreamWriter *xml) = 0; + /// @} +}; + +#endif // COCKATRICE_ABSTRACT_DECK_LIST_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp new file mode 100644 index 000000000..1791c09a6 --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.cpp @@ -0,0 +1,8 @@ +#include "deck_list_card_node.h" + +DecklistCardNode::DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent) + : AbstractDecklistCardNode(_parent), name(other->getName()), number(other->getNumber()), + cardSetShortName(other->getCardSetShortName()), cardSetNumber(other->getCardCollectorNumber()), + cardProviderId(other->getCardProviderId()) +{ +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h new file mode 100644 index 000000000..b3d42b89a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h @@ -0,0 +1,186 @@ +/** + * @file deck_list_card_node.h + * @brief Defines the DecklistCardNode class, representing a single card entry + * in the deck list tree. + * + * DecklistCardNode is the concrete data-bearing node that corresponds to + * an individual card entry in a deck. It stores the card’s name, quantity, + * set information, and provider ID. These nodes live inside an + * InnerDecklistNode (e.g., under Mainboard → Group → Card). + */ + +#ifndef COCKATRICE_DECK_LIST_CARD_NODE_H +#define COCKATRICE_DECK_LIST_CARD_NODE_H + +#include "abstract_deck_list_card_node.h" + +#include + +/** + * @class DecklistCardNode + * @ingroup DeckModels + * @brief Concrete node type representing an actual card entry in the deck. + * + * This class extends AbstractDecklistCardNode to hold all information + * needed to uniquely identify a card printing within the deck. + * + * ### Role in the hierarchy: + * - Child of an InnerDecklistNode (which groups cards by zone or criteria). + * - Leaf node in the deck tree; it does not contain further children. + * + * ### Data stored: + * - @c name: Card’s display name. + * - @c number: Quantity of this card in the deck. + * - @c cardSetShortName: Abbreviation of the set (e.g., "NEO" for Neon Dynasty). + * - @c cardSetNumber: Collector number within the set. + * - @c cardProviderId: External provider identifier (e.g., UUID or MTGJSON ID). + * + * ### Usage: + * - Constructed directly when building a deck list from user input or file. + * - Used by DeckListModel to present cards in Qt views. + * - Convertible to @c CardRef for database lookups or cross-references. + * + * ### Ownership: + * - Owned by its parent InnerDecklistNode. + * - Destroyed automatically when its parent is destroyed. + */ +class DecklistCardNode : public AbstractDecklistCardNode +{ + QString name; ///< Display name of the card. + int number; ///< Quantity of this card in the deck. + QString cardSetShortName; ///< Short set code (e.g., "NEO"). + QString cardSetNumber; ///< Collector number within the set. + QString cardProviderId; ///< External provider identifier (e.g., UUID). + bool formatLegal; ///< Format legality + +public: + /** + * @brief Construct a new DecklistCardNode. + * + * @param _name Display name of the card. + * @param _number Quantity of this card (default = 1). + * @param _parent Parent node in the tree (zone or group). May be nullptr. + * @param position Index to insert into parent’s children. -1 = append. + * @param _cardSetShortName Short set code (e.g., "NEO"). + * @param _cardSetNumber Collector number within the set. + * @param _cardProviderId External provider ID (e.g., UUID). + * @param _formatLegality If the card is legal in the format + * + * On construction, if a parent is provided, this node is inserted into + * the parent’s children list automatically. + */ + explicit DecklistCardNode(QString _name = QString(), + int _number = 1, + InnerDecklistNode *_parent = nullptr, + int position = -1, + QString _cardSetShortName = QString(), + QString _cardSetNumber = QString(), + QString _cardProviderId = QString(), + bool _formatLegality = true) + : AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number), + cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)), + cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality) + { + } + + /** + * @brief Copy constructor with new parent assignment. + * @param other Existing DecklistCardNode to copy. + * @param _parent Parent node for the copy. + * + * Creates a deep copy of the card node’s properties, but attaches + * the new instance to a different parent in the tree. + */ + explicit DecklistCardNode(DecklistCardNode *other, InnerDecklistNode *_parent); + + /// @return The quantity of this card. + [[nodiscard]] int getNumber() const override + { + return number; + } + + /// @param _number Set the quantity of this card. + void setNumber(int _number) override + { + number = _number; + } + + /// @return The display name of this card. + [[nodiscard]] QString getName() const override + { + return name; + } + + /// @param _name Set the display name of this card. + void setName(const QString &_name) override + { + name = _name; + } + + /// @return The provider identifier for this card. + [[nodiscard]] QString getCardProviderId() const override + { + return cardProviderId; + } + + /// @param _providerId Set the provider identifier for this card. + void setCardProviderId(const QString &_providerId) override + { + cardProviderId = _providerId; + } + + /// @return The short set code (e.g., "NEO"). + [[nodiscard]] QString getCardSetShortName() const override + { + return cardSetShortName; + } + + /// @param _cardSetShortName Set the short set code. + void setCardSetShortName(const QString &_cardSetShortName) override + { + cardSetShortName = _cardSetShortName; + } + + /// @return The collector number of this card within its set. + [[nodiscard]] QString getCardCollectorNumber() const override + { + return cardSetNumber; + } + + /// @param _cardSetNumber Set the collector number. + void setCardCollectorNumber(const QString &_cardSetNumber) override + { + cardSetNumber = _cardSetNumber; + } + + /// @return The format legality of the card + [[nodiscard]] bool getFormatLegality() const override + { + return formatLegal; + } + + /// @param _formatLegal If the card is considered legal + void setFormatLegality(const bool _formatLegal) override + { + formatLegal = _formatLegal; + } + + /// @return Always false; card nodes are not deck headers. + [[nodiscard]] bool isDeckHeader() const override + { + return false; + } + + /** + * @brief Convert this node to a CardRef. + * + * @return A CardRef with the card’s name and provider ID, suitable + * for database lookups or comparison with other card sources. + */ + [[nodiscard]] CardRef toCardRef() const + { + return {name, cardProviderId}; + } +}; + +#endif // COCKATRICE_DECK_LIST_CARD_NODE_H diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp new file mode 100644 index 000000000..eca58963a --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.cpp @@ -0,0 +1,199 @@ +#include "inner_deck_list_node.h" + +#include "deck_list_card_node.h" + +InnerDecklistNode::InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent) + : AbstractDecklistNode(_parent), name(other->getName()) +{ + for (int i = 0; i < other->size(); ++i) { + auto *inner = dynamic_cast(other->at(i)); + if (inner) { + new InnerDecklistNode(inner, this); + } else { + new DecklistCardNode(dynamic_cast(other->at(i)), this); + } + } +} + +InnerDecklistNode::~InnerDecklistNode() +{ + clearTree(); +} + +QString InnerDecklistNode::visibleNameFromName(const QString &_name) +{ + if (_name == DECK_ZONE_MAIN) { + return QObject::tr("Maindeck"); + } else if (_name == DECK_ZONE_SIDE) { + return QObject::tr("Sideboard"); + } else if (_name == DECK_ZONE_TOKENS) { + return QObject::tr("Tokens"); + } else { + return _name; + } +} + +void InnerDecklistNode::setSortMethod(DeckSortMethod method) +{ + sortMethod = method; + for (int i = 0; i < size(); i++) { + at(i)->setSortMethod(method); + } +} + +QString InnerDecklistNode::getVisibleName() const +{ + return visibleNameFromName(name); +} + +void InnerDecklistNode::clearTree() +{ + for (int i = 0; i < size(); i++) + delete at(i); + clear(); +} + +AbstractDecklistNode *InnerDecklistNode::findChild(const QString &_name) +{ + for (int i = 0; i < size(); i++) { + if (at(i)->getName() == _name) { + return at(i); + } + } + return nullptr; +} + +AbstractDecklistNode *InnerDecklistNode::findCardChildByNameProviderIdAndNumber(const QString &_name, + const QString &_providerId, + const QString &_cardNumber) +{ + for (const auto &i : *this) { + if (!i || i->getName() != _name) { + continue; + } + if (_cardNumber != "" && i->getCardCollectorNumber() != _cardNumber) { + continue; + } + if (_providerId != "" && i->getCardProviderId() != _providerId) { + continue; + } + return i; + } + return nullptr; +} + +int InnerDecklistNode::height() const +{ + return at(0)->height() + 1; +} + +int InnerDecklistNode::recursiveCount(bool countTotalCards) const +{ + int result = 0; + for (int i = 0; i < size(); i++) { + auto *node = dynamic_cast(at(i)); + + if (node) { + result += node->recursiveCount(countTotalCards); + } else if (countTotalCards) { + result += dynamic_cast(at(i))->getNumber(); + } else { + result++; + } + } + return result; +} + +bool InnerDecklistNode::compare(AbstractDecklistNode *other) const +{ + switch (sortMethod) { + case ByNumber: + return compareNumber(other); + case ByName: + return compareName(other); + default: + return false; + } +} + +bool InnerDecklistNode::compareNumber(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + int n1 = recursiveCount(true); + int n2 = other2->recursiveCount(true); + return (n1 != n2) ? (n1 > n2) : compareName(other); + } else { + return false; + } +} + +bool InnerDecklistNode::compareName(AbstractDecklistNode *other) const +{ + auto *other2 = dynamic_cast(other); + if (other2) { + return (getName() > other2->getName()); + } else { + return false; + } +} + +bool InnerDecklistNode::readElement(QXmlStreamReader *xml) +{ + while (!xml->atEnd()) { + xml->readNext(); + const QString childName = xml->name().toString(); + if (xml->isStartElement()) { + if (childName == "zone") { + auto *newZone = new InnerDecklistNode(xml->attributes().value("name").toString(), this); + newZone->readElement(xml); + } else if (childName == "card") { + auto *newCard = new DecklistCardNode( + xml->attributes().value("name").toString(), xml->attributes().value("number").toString().toInt(), + this, -1, xml->attributes().value("setShortName").toString(), + xml->attributes().value("collectorNumber").toString(), xml->attributes().value("uuid").toString()); + newCard->readElement(xml); + } + } else if (xml->isEndElement() && (childName == "zone")) + return false; + } + return true; +} + +void InnerDecklistNode::writeElement(QXmlStreamWriter *xml) +{ + xml->writeStartElement("zone"); + xml->writeAttribute("name", name); + for (int i = 0; i < size(); i++) + at(i)->writeElement(xml); + xml->writeEndElement(); // zone +} + +QVector> InnerDecklistNode::sort(Qt::SortOrder order) +{ + QVector> result(size()); + + // Initialize temporary list with contents of current list + QVector> tempList(size()); + for (int i = size() - 1; i >= 0; --i) { + tempList[i].first = i; + tempList[i].second = at(i); + } + + // Sort temporary list + auto cmp = [order](const auto &a, const auto &b) { + return (order == Qt::AscendingOrder) ? (b.second->compare(a.second)) : (a.second->compare(b.second)); + }; + + std::sort(tempList.begin(), tempList.end(), cmp); + + // Map old indexes to new indexes and + // copy temporary list to the current one + for (int i = size() - 1; i >= 0; --i) { + result[i].first = tempList[i].first; + result[i].second = i; + replace(i, tempList[i].second); + } + + return result; +} \ No newline at end of file diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h new file mode 100644 index 000000000..f4c48afce --- /dev/null +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/inner_deck_list_node.h @@ -0,0 +1,228 @@ +/** + * @file inner_deck_list_node.h + * @brief Defines the InnerDecklistNode class, which represents + * structural nodes (zones and groups) in the deck tree. + * + * The deck tree consists of: + * - A root node (invisible). + * - Zones (Main, Sideboard, Tokens). + * - Optional grouping nodes (e.g., by type, color, or mana cost). + * - Card nodes as leaves. + * + * InnerDecklistNode implements the zone/group nodes and provides + * storage and management of child nodes. + */ + +#ifndef COCKATRICE_INNER_DECK_LIST_NODE_H +#define COCKATRICE_INNER_DECK_LIST_NODE_H + +#include "abstract_deck_list_node.h" + +/// Constant for the "main" deck zone name. +#define DECK_ZONE_MAIN "main" +/// Constant for the "sideboard" zone name. +#define DECK_ZONE_SIDE "side" +/// Constant for the "tokens" zone name. +#define DECK_ZONE_TOKENS "tokens" + +/** + * @class InnerDecklistNode + * @brief Represents a container node in the deck list hierarchy + * (zones and groupings). + * + * Unlike DecklistCardNode, which holds leaf card data, this class + * manages collections of child nodes, which may themselves be + * InnerDecklistNode or DecklistCardNode objects. + * + * ### Role in the hierarchy: + * - Root node (invisible): Holds zones. + * - Zone nodes: "main", "side", "tokens". + * - Grouping nodes: Created dynamically when grouping by type, + * color, or mana cost. + * - Card nodes: Always children of an InnerDecklistNode. + * + * ### Design notes: + * - Inherits from AbstractDecklistNode (tree interface) and + * QList (storage of children). + * This allows direct QList-style manipulation of children while + * still presenting a polymorphic node interface. + * + * ### Responsibilities: + * - Store a display name. + * - Own and manage child nodes (insert, clear, find). + * - Provide recursive operations such as counting cards or computing height. + * - Implement sorting logic for reordering children. + * - Implement XML serialization for persistence. + * + * ### Ownership: + * - Owns all child nodes stored in the QList. The destructor + * recursively deletes children. + */ +class InnerDecklistNode : public AbstractDecklistNode, public QList +{ + QString name; ///< Internal identifier for this node (zone or group name). + +public: + /** + * @brief Construct a new InnerDecklistNode. + * + * @param _name Internal name (e.g., "main", "side", "tokens", or group label). + * @param _parent Parent node (may be nullptr for the root). + * @param position Optional index for insertion into parent. -1 = append. + */ + explicit InnerDecklistNode(QString _name = QString(), InnerDecklistNode *_parent = nullptr, int position = -1) + : AbstractDecklistNode(_parent, position), name(std::move(_name)) + { + } + + /** + * @brief Copy constructor with parent reassignment. + * @param other Node to copy from (deep copy of children). + * @param _parent Parent node for the copy. + */ + explicit InnerDecklistNode(InnerDecklistNode *other, InnerDecklistNode *_parent = nullptr); + + /** + * @brief Destructor. Recursively deletes all child nodes. + */ + ~InnerDecklistNode() override; + + /** + * @brief Set the sorting method for this node and all children. + * @param method Sort method to apply recursively. + */ + void setSortMethod(DeckSortMethod method) override; + + /// @return The internal name of this node. + [[nodiscard]] QString getName() const override + { + return name; + } + + /// @param _name Set the internal name of this node. + void setName(const QString &_name) + { + name = _name; + } + + /** + * @brief Translate an internal name into a user-visible name. + * + * For example, the internal string "main" is presented as + * "Mainboard" in the UI. + * + * @param _name Internal identifier. + * @return Display-friendly string. + */ + static QString visibleNameFromName(const QString &_name); + + /** + * @brief Get this node’s display-friendly name. + * @return Human-readable name (zone/group name). + */ + [[nodiscard]] virtual QString getVisibleName() const; + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardProviderId() const override + { + return ""; + } + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardSetShortName() const override + { + return ""; + } + + /// @return Always empty for container nodes. + [[nodiscard]] QString getCardCollectorNumber() const override + { + return ""; + } + + /// @return Always true; InnerDecklistNode represents deck structure. + [[nodiscard]] bool isDeckHeader() const override + { + return true; + } + + /** + * @brief Delete all children of this node, recursively. + */ + void clearTree(); + + /** + * @brief Find a direct child node by name. + * @param _name Name to match. + * @return Pointer to child node, or nullptr if not found. + */ + AbstractDecklistNode *findChild(const QString &_name); + + /** + * @brief Find a child card node by name, provider ID, and collector number. + * + * Searches immediate children only. + * + * @param _name Card name to match. + * @param _providerId Optional provider ID to match. + * @param _cardNumber Optional collector number to match. + * @return Pointer to child node if found, nullptr otherwise. + */ + AbstractDecklistNode *findCardChildByNameProviderIdAndNumber(const QString &_name, + const QString &_providerId = "", + const QString &_cardNumber = ""); + + /** + * @brief Compute the height of this node. + * @return Maximum depth of descendants + 1. + */ + [[nodiscard]] int height() const override; + + /** + * @brief Count cards recursively under this node. + * @param countTotalCards If true, sums up quantities of cards. + * If false, counts unique card nodes. + * @return Total count. + */ + [[nodiscard]] int recursiveCount(bool countTotalCards = false) const; + + /** + * @brief Compare this node against another for sorting. + * + * Uses current @c sortMethod to determine the comparison. + * + * @param other Node to compare. + * @return true if this node should sort before @p other. + */ + bool compare(AbstractDecklistNode *other) const override; + + /// @copydoc compare(AbstractDecklistNode*) const + bool compareNumber(AbstractDecklistNode *other) const; + + /// @copydoc compare(AbstractDecklistNode*) const + bool compareName(AbstractDecklistNode *other) const; + + /** + * @brief Sort this node’s children recursively. + * + * @param order Ascending or descending. + * @return A QVector of (oldIndex, newIndex) pairs indicating + * how children were reordered. + */ + QVector> sort(Qt::SortOrder order = Qt::AscendingOrder); + + /** + * @brief Deserialize this node and its children from XML. + * @param xml Reader positioned at this element. + * @return true if parsing succeeded. + */ + bool readElement(QXmlStreamReader *xml) override; + + /** + * @brief Serialize this node and its children to XML. + * @param xml Writer to append elements to. + */ + void writeElement(QXmlStreamWriter *xml) override; +}; + +#endif // COCKATRICE_INNER_DECK_LIST_NODE_H diff --git a/libcockatrice_filters/CMakeLists.txt b/libcockatrice_filters/CMakeLists.txt new file mode 100644 index 000000000..74566ca05 --- /dev/null +++ b/libcockatrice_filters/CMakeLists.txt @@ -0,0 +1,24 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS libcockatrice/filters/filter_card.h libcockatrice/filters/filter_string.h + libcockatrice/filters/filter_tree.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_filters STATIC ${MOC_SOURCES} libcockatrice/filters/filter_card.cpp + libcockatrice/filters/filter_string.cpp libcockatrice/filters/filter_tree.cpp +) + +add_dependencies(libcockatrice_filters libcockatrice_card) + +target_include_directories(libcockatrice_filters PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_filters PUBLIC libcockatrice_card ${QT_CORE_MODULE}) diff --git a/cockatrice/src/game/filters/filter_card.cpp b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp similarity index 97% rename from cockatrice/src/game/filters/filter_card.cpp rename to libcockatrice_filters/libcockatrice/filters/filter_card.cpp index d49a1b9cb..5fdce7ae0 100644 --- a/cockatrice/src/game/filters/filter_card.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.cpp @@ -60,6 +60,8 @@ const QString CardFilter::attrName(Attr a) switch (a) { case AttrName: return tr("Name"); + case AttrNameExact: + return tr("Name (Exact)"); case AttrType: return tr("Type"); case AttrColor: diff --git a/cockatrice/src/game/filters/filter_card.h b/libcockatrice_filters/libcockatrice/filters/filter_card.h similarity index 77% rename from cockatrice/src/game/filters/filter_card.h rename to libcockatrice_filters/libcockatrice/filters/filter_card.h index 210d5c062..732e43a09 100644 --- a/cockatrice/src/game/filters/filter_card.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_card.h @@ -1,9 +1,14 @@ +/** + * @file filter_card.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + #ifndef CARDFILTER_H #define CARDFILTER_H #include #include -#include class CardFilter : public QObject { @@ -28,6 +33,7 @@ public: AttrLoyalty, AttrManaCost, AttrName, + AttrNameExact, AttrPow, AttrRarity, AttrSet, @@ -46,22 +52,24 @@ private: enum Attr a; public: - CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr){}; + CardFilter(QString &term, Type type, Attr attr) : trm(term), t(type), a(attr) + { + } - Type type() const + [[nodiscard]] Type type() const { return t; } - const QString &term() const + [[nodiscard]] const QString &term() const { return trm; } - Attr attr() const + [[nodiscard]] Attr attr() const { return a; } - QJsonObject toJson() const; + [[nodiscard]] QJsonObject toJson() const; static CardFilter *fromJson(const QJsonObject &json); static const QString typeName(Type t); static const QString attrName(Attr a); diff --git a/cockatrice/src/game/filters/filter_string.cpp b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp similarity index 94% rename from cockatrice/src/game/filters/filter_string.cpp rename to libcockatrice_filters/libcockatrice/filters/filter_string.cpp index 4a7a58b9e..704e8fadb 100644 --- a/cockatrice/src/game/filters/filter_string.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.cpp @@ -1,12 +1,11 @@ #include "filter_string.h" -#include "../../../../common/lib/peglib.h" - #include #include #include #include #include +#include static peg::parser search(R"( Start <- QueryPartList @@ -47,7 +46,7 @@ FieldQuery <- String [:] MatcherString / String ws? NumericExpression NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["]. NonSingleQuoteUnlessEscaped <- "\\\'". / ![']. -UnescapedStringListPart <- !['":<>=! ]. +UnescapedStringListPart <- !['":<>()=! ]. SingleApostropheString <- (UnescapedStringListPart+ ws*)* ['] (UnescapedStringListPart+ ws*)* String <- SingleApostropheString / UnescapedStringListPart+ / ["] ["] / ['] ['] StringValue <- String / [(] StringList [)] @@ -144,13 +143,12 @@ static void setupParserRules() search["FormatQuery"] = [](const peg::SemanticValues &sv) -> Filter { if (sv.choice() == 0) { const auto format = std::any_cast(sv[0]); - return - [=](const CardData &x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == "legal"; }; + return [=](const CardData &x) -> bool { return x->getLegalityProp(format) == "legal"; }; } const auto format = std::any_cast(sv[1]); const auto legality = std::any_cast(sv[0]); - return [=](const CardData &x) -> bool { return x->getProperty(QString("format-%1").arg(format)) == legality; }; + return [=](const CardData &x) -> bool { return x->getLegalityProp(format) == legality; }; }; search["Legality"] = [](const peg::SemanticValues &sv) -> QString { switch (tolower(std::string(sv.sv())[0])) { @@ -187,15 +185,25 @@ static void setupParserRules() return QString::fromStdString(std::string(sv.sv())).toLower(); }; + search["StringValue"] = [](const peg::SemanticValues &sv) -> StringMatcher { + // Helper function for word boundary matching + auto createWordBoundaryMatcher = [](const QString &target) { + QString pattern = QString("\\b%1\\b").arg(QRegularExpression::escape(target)); + QRegularExpression regex(pattern, QRegularExpression::CaseInsensitiveOption); + return [regex](const QString &s) { return regex.match(s).hasMatch(); }; + }; + if (sv.choice() == 0) { const auto target = std::any_cast(sv[0]); - return [=](const QString &s) { return s.split(" ").contains(target, Qt::CaseInsensitive); }; + return createWordBoundaryMatcher(target); } const auto target = std::any_cast(sv[0]); return [=](const QString &s) { - auto containsString = [&s](const QString &str) { return s.split(" ").contains(str, Qt::CaseInsensitive); }; + auto containsString = [&s, &createWordBoundaryMatcher](const QString &str) { + return createWordBoundaryMatcher(str)(s); + }; return std::any_of(target.begin(), target.end(), containsString); }; }; diff --git a/cockatrice/src/game/filters/filter_string.h b/libcockatrice_filters/libcockatrice/filters/filter_string.h similarity index 83% rename from cockatrice/src/game/filters/filter_string.h rename to libcockatrice_filters/libcockatrice/filters/filter_string.h index 131c9f7bf..8fc41ce68 100644 --- a/cockatrice/src/game/filters/filter_string.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_string.h @@ -1,13 +1,19 @@ +/** + * @file filter_string.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + #ifndef FILTER_STRING_H #define FILTER_STRING_H -#include "../cards/card_info.h" #include "filter_tree.h" #include #include #include #include +#include #include inline Q_LOGGING_CATEGORY(FilterStringLog, "filter_string"); @@ -29,7 +35,7 @@ class FilterString public: FilterString(); explicit FilterString(const QString &exp); - bool check(const CardData &card) const + [[nodiscard]] bool check(const CardData &card) const { if (card.isNull()) { static CardInfoPtr blankCard = CardInfo::newInstance(""); diff --git a/cockatrice/src/game/filters/filter_tree.cpp b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp similarity index 98% rename from cockatrice/src/game/filters/filter_tree.cpp rename to libcockatrice_filters/libcockatrice/filters/filter_tree.cpp index 89b772b8e..19e8c2d8d 100644 --- a/cockatrice/src/game/filters/filter_tree.cpp +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.cpp @@ -153,6 +153,11 @@ bool FilterItem::acceptName(const CardInfoPtr info) const return info->getName().contains(term, Qt::CaseInsensitive); } +bool FilterItem::acceptNameExact(const CardInfoPtr info) const +{ + return info->getName() == term; +} + bool FilterItem::acceptType(const CardInfoPtr info) const { return info->getCardType().contains(term, Qt::CaseInsensitive); @@ -270,7 +275,7 @@ bool FilterItem::acceptCmc(const CardInfoPtr info) const bool FilterItem::acceptFormat(const CardInfoPtr info) const { - return info->getProperty(QString("format-%1").arg(term.toLower())) == "legal"; + return info->getLegalityProp(term.toLower()) == "legal"; } bool FilterItem::acceptLoyalty(const CardInfoPtr info) const @@ -401,6 +406,8 @@ bool FilterItem::acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) c switch (attr) { case CardFilter::AttrName: return acceptName(info); + case CardFilter::AttrNameExact: + return acceptNameExact(info); case CardFilter::AttrType: return acceptType(info); case CardFilter::AttrColor: diff --git a/cockatrice/src/game/filters/filter_tree.h b/libcockatrice_filters/libcockatrice/filters/filter_tree.h similarity index 62% rename from cockatrice/src/game/filters/filter_tree.h rename to libcockatrice_filters/libcockatrice/filters/filter_tree.h index 394e98e7b..75d4241a5 100644 --- a/cockatrice/src/game/filters/filter_tree.h +++ b/libcockatrice_filters/libcockatrice/filters/filter_tree.h @@ -1,12 +1,17 @@ +/** + * @file filter_tree.h + * @ingroup CardDatabaseModelFilters + * @brief TODO: Document this. + */ + #ifndef FILTERTREE_H #define FILTERTREE_H -#include "../cards/card_database.h" #include "filter_card.h" #include -#include #include +#include #include class FilterTreeNode @@ -18,7 +23,7 @@ public: FilterTreeNode() : enabled(true) { } - virtual bool isEnabled() const + [[nodiscard]] virtual bool isEnabled() const { return enabled; } @@ -32,18 +37,18 @@ public: enabled = false; nodeChanged(); } - virtual FilterTreeNode *parent() const + [[nodiscard]] virtual FilterTreeNode *parent() const { return nullptr; } - virtual FilterTreeNode *nodeAt(int /* i */) const + [[nodiscard]] virtual FilterTreeNode *nodeAt(int /* i */) const { return nullptr; } virtual void deleteAt(int /* i */) { } - virtual int childCount() const + [[nodiscard]] virtual int childCount() const { return 0; } @@ -51,15 +56,15 @@ public: { return -1; } - virtual int index() const + [[nodiscard]] virtual int index() const { return (parent() != nullptr) ? parent()->childIndex(this) : -1; } - virtual const QString text() const + [[nodiscard]] virtual const QString text() const { return QString(""); } - virtual bool isLeaf() const + [[nodiscard]] virtual bool isLeaf() const { return false; } @@ -98,9 +103,9 @@ protected: public: virtual ~FilterTreeBranch(); void removeFiltersByAttr(CardFilter::Attr filterType); - FilterTreeNode *nodeAt(int i) const override; + [[nodiscard]] FilterTreeNode *nodeAt(int i) const override; void deleteAt(int i) override; - int childCount() const override + [[nodiscard]] int childCount() const override { return childNodes.size(); } @@ -121,10 +126,10 @@ public: LogicMap(CardFilter::Attr a, FilterTree *parent) : p(parent), attr(a) { } - const FilterItemList *findTypeList(CardFilter::Type type) const; + [[nodiscard]] const FilterItemList *findTypeList(CardFilter::Type type) const; FilterItemList *typeList(CardFilter::Type type); - FilterTreeNode *parent() const override; - const QString text() const override + [[nodiscard]] FilterTreeNode *parent() const override; + [[nodiscard]] const QString text() const override { return CardFilter::attrName(attr); } @@ -142,25 +147,25 @@ public: FilterItemList(CardFilter::Type t, LogicMap *parent) : p(parent), type(t) { } - CardFilter::Attr attr() const + [[nodiscard]] CardFilter::Attr attr() const { return p->attr; } - FilterTreeNode *parent() const override + [[nodiscard]] FilterTreeNode *parent() const override { return p; } - int termIndex(const QString &term) const; + [[nodiscard]] int termIndex(const QString &term) const; FilterTreeNode *termNode(const QString &term); - const QString text() const override + [[nodiscard]] const QString text() const override { return CardFilter::typeName(type); } - bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const; }; class FilterItem : public FilterTreeNode @@ -176,42 +181,43 @@ public: } virtual ~FilterItem() = default; - CardFilter::Attr attr() const + [[nodiscard]] CardFilter::Attr attr() const { return p->attr(); } - CardFilter::Type type() const + [[nodiscard]] CardFilter::Type type() const { return p->type; } - FilterTreeNode *parent() const override + [[nodiscard]] FilterTreeNode *parent() const override { return p; } - const QString text() const override + [[nodiscard]] const QString text() const override { return term; } - bool isLeaf() const override + [[nodiscard]] bool isLeaf() const override { return true; } - bool acceptName(CardInfoPtr info) const; - bool acceptType(CardInfoPtr info) const; - bool acceptMainType(CardInfoPtr info) const; - bool acceptSubType(CardInfoPtr info) const; - bool acceptColor(CardInfoPtr info) const; - bool acceptText(CardInfoPtr info) const; - bool acceptSet(CardInfoPtr info) const; - bool acceptManaCost(CardInfoPtr info) const; - bool acceptCmc(CardInfoPtr info) const; - bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; - bool acceptLoyalty(CardInfoPtr info) const; - bool acceptRarity(CardInfoPtr info) const; - bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const; - bool acceptFormat(CardInfoPtr info) const; - bool relationCheck(int cardInfo) const; + [[nodiscard]] bool acceptName(CardInfoPtr info) const; + [[nodiscard]] bool acceptNameExact(CardInfoPtr info) const; + [[nodiscard]] bool acceptType(CardInfoPtr info) const; + [[nodiscard]] bool acceptMainType(CardInfoPtr info) const; + [[nodiscard]] bool acceptSubType(CardInfoPtr info) const; + [[nodiscard]] bool acceptColor(CardInfoPtr info) const; + [[nodiscard]] bool acceptText(CardInfoPtr info) const; + [[nodiscard]] bool acceptSet(CardInfoPtr info) const; + [[nodiscard]] bool acceptManaCost(CardInfoPtr info) const; + [[nodiscard]] bool acceptCmc(CardInfoPtr info) const; + [[nodiscard]] bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool acceptLoyalty(CardInfoPtr info) const; + [[nodiscard]] bool acceptRarity(CardInfoPtr info) const; + [[nodiscard]] bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const; + [[nodiscard]] bool acceptFormat(CardInfoPtr info) const; + [[nodiscard]] bool relationCheck(int cardInfo) const; }; class FilterTree : public QObject, public FilterTreeBranch @@ -259,16 +265,16 @@ public: FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term); FilterTreeNode *termNode(const CardFilter *f); - const QString text() const override + [[nodiscard]] const QString text() const override { return QString("root"); } - int index() const override + [[nodiscard]] int index() const override { return 0; } - bool acceptsCard(CardInfoPtr info) const; + [[nodiscard]] bool acceptsCard(CardInfoPtr info) const; void removeFiltersByAttr(CardFilter::Attr filterType); void removeFilter(const CardFilter *toRemove); void clear(); diff --git a/libcockatrice_interfaces/CMakeLists.txt b/libcockatrice_interfaces/CMakeLists.txt new file mode 100644 index 000000000..4f34f7985 --- /dev/null +++ b/libcockatrice_interfaces/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/interfaces/interface_card_database_path_provider.h + libcockatrice/interfaces/interface_card_preference_provider.h + libcockatrice/interfaces/interface_card_set_priority_controller.h + libcockatrice/interfaces/interface_network_settings_provider.h + libcockatrice/interfaces/noop_card_database_path_provider.h + libcockatrice/interfaces/noop_card_preference_provider.h + libcockatrice/interfaces/noop_card_set_priority_controller.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES}) + +target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE}) diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h new file mode 100644 index 000000000..dd55ab789 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_database_path_provider.h @@ -0,0 +1,21 @@ +#ifndef COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H +#include + +class ICardDatabasePathProvider : public QObject +{ + Q_OBJECT + +public: + virtual ~ICardDatabasePathProvider() = default; + + [[nodiscard]] virtual QString getCardDatabasePath() const = 0; + [[nodiscard]] virtual QString getCustomCardDatabasePath() const = 0; + [[nodiscard]] virtual QString getTokenDatabasePath() const = 0; + [[nodiscard]] virtual QString getSpoilerCardDatabasePath() const = 0; + +signals: + void cardDatabasePathChanged(); +}; + +#endif // COCKATRICE_INTERFACE_CARD_DATABASE_PATH_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h new file mode 100644 index 000000000..a6cf941fb --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_preference_provider.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H +#define COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H + +#include + +class ICardPreferenceProvider +{ +public: + virtual ~ICardPreferenceProvider() = default; + [[nodiscard]] virtual QString getCardPreferenceOverride(const QString &cardName) const = 0; + [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; +}; + +#endif // COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h new file mode 100644 index 000000000..b8fbbc74a --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_card_set_priority_controller.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H +#define COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H + +#include + +class ICardSetPriorityController +{ +public: + virtual ~ICardSetPriorityController() = default; + + virtual void setSortKey(QString shortName, unsigned int sortKey) = 0; + virtual void setEnabled(QString shortName, bool enabled) = 0; + virtual void setIsKnown(QString shortName, bool isknown) = 0; + + virtual unsigned int getSortKey(QString shortName) const = 0; + virtual bool isEnabled(QString shortName) const = 0; + virtual bool isKnown(QString shortName) const = 0; +}; + +#endif // COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h new file mode 100644 index 000000000..bd136513d --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_network_settings_provider.h @@ -0,0 +1,20 @@ +#ifndef COCKATRICE_INETWORKSETTINGSPROVIDER_H +#define COCKATRICE_INETWORKSETTINGSPROVIDER_H +#include + +class INetworkSettingsProvider +{ +public: + virtual ~INetworkSettingsProvider() = default; + + virtual QString getClientID() = 0; + + [[nodiscard]] virtual int getTimeOut() const = 0; + [[nodiscard]] virtual int getKeepAlive() const = 0; + [[nodiscard]] virtual bool getNotifyAboutUpdates() const = 0; + + virtual void setKnownMissingFeatures(const QString &_knownMissingFeatures) = 0; + virtual QString getKnownMissingFeatures() = 0; +}; + +#endif // COCKATRICE_INETWORKSETTINGSPROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h new file mode 100644 index 000000000..bacd7a2bb --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_database_path_provider.h @@ -0,0 +1,26 @@ +#ifndef COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H +#include "interface_card_database_path_provider.h" + +class NoopCardDatabasePathProvider : public ICardDatabasePathProvider +{ +public: + [[nodiscard]] QString getCardDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getCustomCardDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getTokenDatabasePath() const override + { + return ""; + } + [[nodiscard]] QString getSpoilerCardDatabasePath() const override + { + return ""; + } +}; + +#endif // COCKATRICE_NOOP_CARD_DATABASE_PATH_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h new file mode 100644 index 000000000..3781f4b63 --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_preference_provider.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H +#define COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H +#include "interface_card_preference_provider.h" + +class NoopCardPreferenceProvider : public ICardPreferenceProvider +{ +public: + [[nodiscard]] QString getCardPreferenceOverride(const QString &) const override + { + return {}; + } + + [[nodiscard]] bool getIncludeRebalancedCards() const override + { + return true; + } +}; + +#endif // COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h new file mode 100644 index 000000000..e5027648c --- /dev/null +++ b/libcockatrice_interfaces/libcockatrice/interfaces/noop_card_set_priority_controller.h @@ -0,0 +1,33 @@ +#ifndef COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H +#define COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H + +#include "interface_card_set_priority_controller.h" + +class NoopCardSetPriorityController : public ICardSetPriorityController +{ +public: + void setSortKey(QString /* shortName */, unsigned int /* sortKey */) override + { + } + void setEnabled(QString /* shortName */, bool /* enabled */) override + { + } + void setIsKnown(QString /* shortName */, bool /* isknown */) override + { + } + + unsigned int getSortKey(QString /* shortName */) const override + { + return 0; + } + bool isEnabled(QString /* shortName */) const override + { + return true; + } + bool isKnown(QString /* shortName */) const override + { + return true; + } +}; + +#endif // COCKATRICE_NOOP_CARD_SET_PRIORITY_CONTROLLER_H diff --git a/libcockatrice_models/CMakeLists.txt b/libcockatrice_models/CMakeLists.txt new file mode 100644 index 000000000..7af35b2d5 --- /dev/null +++ b/libcockatrice_models/CMakeLists.txt @@ -0,0 +1,12 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(libcockatrice/models/database) +add_subdirectory(libcockatrice/models/deck_list) + +add_library(libcockatrice_models INTERFACE) + +target_include_directories(libcockatrice_models INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_models INTERFACE libcockatrice_models_database libcockatrice_models_deck_list) diff --git a/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt new file mode 100644 index 000000000..950d6d79f --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/CMakeLists.txt @@ -0,0 +1,35 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + card_database_model.h + card_database_display_model.h + card/card_completer_proxy_model.h + card/card_search_model.h + card_set/card_sets_model.h + token/token_display_model.h + token/token_edit_model.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_models_database STATIC + ${MOC_SOURCES} + card_database_model.cpp + card_database_display_model.cpp + card/card_completer_proxy_model.cpp + card/card_search_model.cpp + card_set/card_sets_model.cpp + token/token_display_model.cpp + token/token_edit_model.cpp +) + +target_include_directories(libcockatrice_models_database PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_models_database PUBLIC libcockatrice_card libcockatrice_filters ${QT_CORE_MODULE}) diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp similarity index 100% rename from cockatrice/src/game/cards/card_completer_proxy_model.cpp rename to libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.cpp diff --git a/cockatrice/src/game/cards/card_completer_proxy_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h similarity index 59% rename from cockatrice/src/game/cards/card_completer_proxy_model.h rename to libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h index 73cf223ed..afb6f1fcf 100644 --- a/cockatrice/src/game/cards/card_completer_proxy_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_completer_proxy_model.h @@ -1,3 +1,9 @@ +/** + * @file card_completer_proxy_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + #ifndef CARD_COMPLETER_PROXY_MODEL_H #define CARD_COMPLETER_PROXY_MODEL_H @@ -10,7 +16,7 @@ public: explicit CardCompleterProxyModel(QObject *parent = nullptr); protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; }; #endif // CARD_COMPLETER_PROXY_MODEL_H diff --git a/cockatrice/src/game/cards/card_search_model.cpp b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp similarity index 94% rename from cockatrice/src/game/cards/card_search_model.cpp rename to libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp index 56b5ea530..6a930c1da 100644 --- a/cockatrice/src/game/cards/card_search_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.cpp @@ -1,8 +1,10 @@ #include "card_search_model.h" -#include "../../utility/levenshtein.h" +#include "../card_database_display_model.h" +#include "../card_database_model.h" #include +#include CardSearchModel::CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent) : QAbstractListModel(parent), sourceModel(sourceModel) diff --git a/cockatrice/src/game/cards/card_search_model.h b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h similarity index 62% rename from cockatrice/src/game/cards/card_search_model.h rename to libcockatrice_models/libcockatrice/models/database/card/card_search_model.h index c0575cb61..18be2c55a 100644 --- a/cockatrice/src/game/cards/card_search_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card/card_search_model.h @@ -1,7 +1,13 @@ +/** + * @file card_search_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + #ifndef CARD_SEARCH_MODEL_H #define CARD_SEARCH_MODEL_H -#include "card_database_model.h" +#include "../card_database_display_model.h" #include @@ -11,8 +17,8 @@ class CardSearchModel : public QAbstractListModel public: explicit CardSearchModel(CardDatabaseDisplayModel *sourceModel, QObject *parent = nullptr); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void updateSearchResults(const QString &query); // Update results based on input diff --git a/cockatrice/src/game/cards/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp similarity index 56% rename from cockatrice/src/game/cards/card_database_model.cpp rename to libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp index 6d99e563b..5ce63a939 100644 --- a/cockatrice/src/game/cards/card_database_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.cpp @@ -1,157 +1,7 @@ +#include "card_database_display_model.h" + #include "card_database_model.h" -#include "../filters/filter_tree.h" - -#include - -#define CARDDBMODEL_COLUMNS 6 - -CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent) - : QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets) -{ - connect(db, &CardDatabase::cardAdded, this, &CardDatabaseModel::cardAdded); - connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved); - connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this, - &CardDatabaseModel::cardDatabaseEnabledSetsChanged); - - cardDatabaseEnabledSetsChanged(); -} - -CardDatabaseModel::~CardDatabaseModel() = default; - -QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, - {L'”', L'\"'}, - {L'‘', L'\''}, - {L'’', L'\''}}; - -int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const -{ - return cardList.size(); -} - -int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const -{ - return CARDDBMODEL_COLUMNS; -} - -QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() >= cardList.size() || index.column() >= CARDDBMODEL_COLUMNS || - (role != Qt::DisplayRole && role != SortRole)) - return QVariant(); - - CardInfoPtr card = cardList.at(index.row()); - switch (index.column()) { - case NameColumn: - return card->getName(); - case SetListColumn: - return card->getSetsNames(); - case ManaCostColumn: - return role == SortRole ? QString("%1%2").arg(card->getCmc(), 4, QChar('0')).arg(card->getManaCost()) - : card->getManaCost(); - case CardTypeColumn: - return card->getCardType(); - case PTColumn: - return card->getPowTough(); - case ColorColumn: - return card->getColors(); - default: - return QVariant(); - } -} - -QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (role != Qt::DisplayRole) - return QVariant(); - if (orientation != Qt::Horizontal) - return QVariant(); - switch (section) { - case NameColumn: - return QString(tr("Name")); - case SetListColumn: - return QString(tr("Sets")); - case ManaCostColumn: - return QString(tr("Mana cost")); - case CardTypeColumn: - return QString(tr("Card type")); - case PTColumn: - return QString(tr("P/T")); - case ColorColumn: - return QString(tr("Color(s)")); - default: - return QVariant(); - } -} - -void CardDatabaseModel::cardInfoChanged(CardInfoPtr card) -{ - const int row = cardList.indexOf(card); - if (row == -1) - return; - - emit dataChanged(index(row, 0), index(row, CARDDBMODEL_COLUMNS - 1)); -} - -bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(CardInfoPtr card) -{ - if (!showOnlyCardsFromEnabledSets) - return true; - - for (const auto &printings : card->getSets()) { - for (const auto &printing : printings) { - if (printing.getSet()->getEnabled()) - return true; - } - } - - return false; -} - -void CardDatabaseModel::cardDatabaseEnabledSetsChanged() -{ - // remove all the cards no more present in at least one enabled set - for (const CardInfoPtr &card : cardList) { - if (!checkCardHasAtLeastOneEnabledSet(card)) { - cardRemoved(card); - } - } - - // re-check all the card currently not shown, maybe their part of a newly-enabled set - for (const CardInfoPtr &card : db->getCardList()) { - if (!cardListSet.contains(card)) { - cardAdded(card); - } - } -} - -void CardDatabaseModel::cardAdded(CardInfoPtr card) -{ - if (checkCardHasAtLeastOneEnabledSet(card)) { - // add the card if it's present in at least one enabled set - beginInsertRows(QModelIndex(), cardList.size(), cardList.size()); - cardList.append(card); - cardListSet.insert(card); - connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged); - endInsertRows(); - } -} - -void CardDatabaseModel::cardRemoved(CardInfoPtr card) -{ - const int row = cardList.indexOf(card); - if (row == -1) { - return; - } - - beginRemoveRows(QModelIndex(), row, row); - disconnect(card.data(), nullptr, this, nullptr); - cardListSet.remove(card); - card.clear(); - cardList.removeAt(row); - endRemoveRows(); -} - CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) : QSortFilterProxyModel(parent), isToken(ShowAll), filterString(nullptr) { @@ -165,6 +15,11 @@ CardDatabaseDisplayModel::CardDatabaseDisplayModel(QObject *parent) loadedRowCount = 0; } +QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, + {L'”', L'\"'}, + {L'‘', L'\''}, + {L'’', L'\''}}; + bool CardDatabaseDisplayModel::canFetchMore(const QModelIndex &index) const { return loadedRowCount < sourceModel()->rowCount(index); @@ -329,13 +184,20 @@ bool CardDatabaseDisplayModel::rowMatchesCardName(CardInfoPtr info) const void CardDatabaseDisplayModel::clearFilterAll() { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) + beginFilterChange(); +#endif cardName.clear(); cardText.clear(); cardTypes.clear(); cardColors.clear(); if (filterTree != nullptr) filterTree->clear(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)) + endFilterChange(QSortFilterProxyModel::Direction::Rows); +#else invalidateFilter(); +#endif } void CardDatabaseDisplayModel::setFilterTree(FilterTree *_filterTree) @@ -363,35 +225,3 @@ const QString CardDatabaseDisplayModel::sanitizeCardName(const QString &dirtyNam } return QString::fromStdWString(toReturn); } - -TokenDisplayModel::TokenDisplayModel(QObject *parent) : CardDatabaseDisplayModel(parent) -{ -} - -bool TokenDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const -{ - CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); - return info->getIsToken() && rowMatchesCardName(info); -} - -int TokenDisplayModel::rowCount(const QModelIndex &parent) const -{ - // always load all tokens at start - return QSortFilterProxyModel::rowCount(parent); -} - -TokenEditModel::TokenEditModel(QObject *parent) : CardDatabaseDisplayModel(parent) -{ -} - -bool TokenEditModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const -{ - CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); - return info->getIsToken() && info->getSets().contains(CardSet::TOKENS_SETNAME) && rowMatchesCardName(info); -} - -int TokenEditModel::rowCount(const QModelIndex &parent) const -{ - // always load all tokens at start - return QSortFilterProxyModel::rowCount(parent); -} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h new file mode 100644 index 000000000..0c5994a3a --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_display_model.h @@ -0,0 +1,96 @@ +/** + * @file card_database_display_model.h + * @ingroup CardDatabaseModels + * @brief The CardDatabaseDisplayModel is a QSortFilterProxyModel that allows applying filters and sorting to a + * CardDatabaseModel. + */ + +#ifndef COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H +#define COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H + +#include +#include +#include + +class FilterTree; +class CardDatabaseDisplayModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + enum FilterBool + { + ShowTrue, + ShowFalse, + ShowAll + }; + +private: + FilterBool isToken; + QString cardName, cardText; + QSet cardNameSet, cardTypes, cardColors; + FilterTree *filterTree; + FilterString *filterString; + int loadedRowCount; + QTimer dirtyTimer; + + /** The translation table that will be used for sanitizeCardName. */ + static QMap characterTranslation; + +public: + explicit CardDatabaseDisplayModel(QObject *parent = nullptr); + void setFilterTree(FilterTree *_filterTree); + void setIsToken(FilterBool _isToken) + { + isToken = _isToken; + emit modelDirty(); + dirty(); + } + + void setCardName(const QString &_cardName) + { + if (filterString != nullptr) { + delete filterString; + filterString = nullptr; + } + cardName = sanitizeCardName(_cardName, characterTranslation); + emit modelDirty(); + dirty(); + } + void setStringFilter(const QString &_src) + { + delete filterString; + filterString = new FilterString(_src); + emit modelDirty(); + dirty(); + } + void setCardNameSet(const QSet &_cardNameSet) + { + cardNameSet = _cardNameSet; + emit modelDirty(); + dirty(); + } + + void dirty() + { + dirtyTimer.start(20); + } + void clearFilterAll(); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; +signals: + void modelDirty(); + +protected: + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + static int lessThanNumerically(const QString &left, const QString &right); + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + [[nodiscard]] bool rowMatchesCardName(CardInfoPtr info) const; + +private slots: + void filterTreeChanged(); + /** Will translate all undesirable characters in DIRTYNAME according to the TABLE. */ + const QString sanitizeCardName(const QString &dirtyName, const QMap &table); +}; + +#endif // COCKATRICE_CARD_DATABASE_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp new file mode 100644 index 000000000..e33156329 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.cpp @@ -0,0 +1,147 @@ +#include "card_database_model.h" + +#include +#include + +#define CARDDBMODEL_COLUMNS 6 + +CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent) + : QAbstractListModel(parent), db(_db), showOnlyCardsFromEnabledSets(_showOnlyCardsFromEnabledSets) +{ + connect(db, &CardDatabase::cardAdded, this, &CardDatabaseModel::cardAdded); + connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseModel::cardRemoved); + connect(db, &CardDatabase::cardDatabaseEnabledSetsChanged, this, + &CardDatabaseModel::cardDatabaseEnabledSetsChanged); + + cardDatabaseEnabledSetsChanged(); +} + +CardDatabaseModel::~CardDatabaseModel() = default; + +int CardDatabaseModel::rowCount(const QModelIndex & /*parent*/) const +{ + return cardList.size(); +} + +int CardDatabaseModel::columnCount(const QModelIndex & /*parent*/) const +{ + return CARDDBMODEL_COLUMNS; +} + +QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= cardList.size() || index.column() >= CARDDBMODEL_COLUMNS || + (role != Qt::DisplayRole && role != SortRole)) + return QVariant(); + + CardInfoPtr card = cardList.at(index.row()); + switch (index.column()) { + case NameColumn: + return card->getName(); + case SetListColumn: + return card->getSetsNames(); + case ManaCostColumn: + return role == SortRole ? QString("%1%2").arg(card->getCmc(), 4, QChar('0')).arg(card->getManaCost()) + : card->getManaCost(); + case CardTypeColumn: + return card->getCardType(); + case PTColumn: + return card->getPowTough(); + case ColorColumn: + return card->getColors(); + default: + return QVariant(); + } +} + +QVariant CardDatabaseModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + if (orientation != Qt::Horizontal) + return QVariant(); + switch (section) { + case NameColumn: + return QString(tr("Name")); + case SetListColumn: + return QString(tr("Sets")); + case ManaCostColumn: + return QString(tr("Mana cost")); + case CardTypeColumn: + return QString(tr("Card type")); + case PTColumn: + return QString(tr("P/T")); + case ColorColumn: + return QString(tr("Color(s)")); + default: + return QVariant(); + } +} + +void CardDatabaseModel::cardInfoChanged(CardInfoPtr card) +{ + const int row = cardList.indexOf(card); + if (row == -1) + return; + + emit dataChanged(index(row, 0), index(row, CARDDBMODEL_COLUMNS - 1)); +} + +bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(CardInfoPtr card) +{ + if (!showOnlyCardsFromEnabledSets) + return true; + + for (const auto &printings : card->getSets()) { + for (const auto &printing : printings) { + if (printing.getSet()->getEnabled()) + return true; + } + } + + return false; +} + +void CardDatabaseModel::cardDatabaseEnabledSetsChanged() +{ + // remove all the cards no more present in at least one enabled set + for (const CardInfoPtr &card : cardList) { + if (!checkCardHasAtLeastOneEnabledSet(card)) { + cardRemoved(card); + } + } + + // re-check all the card currently not shown, maybe their part of a newly-enabled set + for (const CardInfoPtr &card : db->getCardList()) { + if (!cardListSet.contains(card)) { + cardAdded(card); + } + } +} + +void CardDatabaseModel::cardAdded(CardInfoPtr card) +{ + if (checkCardHasAtLeastOneEnabledSet(card)) { + // add the card if it's present in at least one enabled set + beginInsertRows(QModelIndex(), cardList.size(), cardList.size()); + cardList.append(card); + cardListSet.insert(card); + connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged); + endInsertRows(); + } +} + +void CardDatabaseModel::cardRemoved(CardInfoPtr card) +{ + const int row = cardList.indexOf(card); + if (row == -1) { + return; + } + + beginRemoveRows(QModelIndex(), row, row); + disconnect(card.data(), nullptr, this, nullptr); + cardListSet.remove(card); + card.clear(); + cardList.removeAt(row); + endRemoveRows(); +} diff --git a/libcockatrice_models/libcockatrice/models/database/card_database_model.h b/libcockatrice_models/libcockatrice/models/database/card_database_model.h new file mode 100644 index 000000000..218cfff92 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/card_database_model.h @@ -0,0 +1,62 @@ +/** + * @file card_database_model.h + * @ingroup CardDatabaseModels + * @brief The CardDatabaseModel maps the cardList contained in the CardDatabase as a QAbstractListModel. + */ + +#ifndef CARDDATABASEMODEL_H +#define CARDDATABASEMODEL_H + +#include +#include +#include +#include + +class CardDatabaseModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + NameColumn, + SetListColumn, + ManaCostColumn, + PTColumn, + CardTypeColumn, + ColorColumn + }; + enum Role + { + SortRole = Qt::UserRole + }; + CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent = nullptr); + ~CardDatabaseModel() override; + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant + headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] CardDatabase *getDatabase() const + { + return db; + } + [[nodiscard]] CardInfoPtr getCard(int index) const + { + return cardList[index]; + } + +private: + QList cardList; + QSet cardListSet; // Supports faster lookups in cardDatabaseEnabledSetsChanged() + CardDatabase *db; + bool showOnlyCardsFromEnabledSets; + + inline bool checkCardHasAtLeastOneEnabledSet(CardInfoPtr card); +private slots: + void cardAdded(CardInfoPtr card); + void cardRemoved(CardInfoPtr card); + void cardInfoChanged(CardInfoPtr card); + void cardDatabaseEnabledSetsChanged(); +}; + +#endif diff --git a/cockatrice/src/client/network/sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp similarity index 97% rename from cockatrice/src/client/network/sets_model.cpp rename to libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index a86b710b5..f6dc4f9cf 100644 --- a/cockatrice/src/client/network/sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -1,4 +1,4 @@ -#include "sets_model.h" +#include "card_sets_model.h" #include @@ -45,7 +45,7 @@ QVariant SetsModel::data(const QModelIndex &index, int role) const switch (index.column()) { case SortKeyCol: - return QString("%1").arg(set->getSortKey(), 8, 10, QChar('0')); + return QString("%1").arg(index.row(), 8, 10, QChar('0')); case IsKnownCol: return set->getIsKnown(); case SetTypeCol: @@ -283,11 +283,7 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) const auto filter = filterRegularExpression(); -#else - const auto filter = filterRegExp(); -#endif return (sourceModel()->data(typeIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) || diff --git a/cockatrice/src/client/network/sets_model.h b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h similarity index 60% rename from cockatrice/src/client/network/sets_model.h rename to libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h index 0bfbe57e0..0ffc5a847 100644 --- a/cockatrice/src/client/network/sets_model.h +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.h @@ -1,12 +1,17 @@ +/** + * @file sets_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + #ifndef SETSMODEL_H #define SETSMODEL_H -#include "../../game/cards/card_database.h" - #include #include #include #include +#include class SetsProxyModel; @@ -20,11 +25,11 @@ public: SetsMimeData(int _oldRow) : oldRow(_oldRow) { } - int getOldRow() const + [[nodiscard]] int getOldRow() const { return oldRow; } - QStringList formats() const + [[nodiscard]] QStringList formats() const { return QStringList() << "application/x-cockatricecardset"; } @@ -37,7 +42,7 @@ class SetsModel : public QAbstractTableModel private: static const int NUM_COLS = 7; - SetList sets; + CardSetList sets; QSet enabledSets; public: @@ -59,22 +64,23 @@ public: explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr); ~SetsModel() override; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + [[nodiscard]] int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent); return NUM_COLS; } - QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; bool setData(const QModelIndex &index, const QVariant &value, int role) override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - Qt::DropActions supportedDropActions() const override; + [[nodiscard]] QVariant + headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; + [[nodiscard]] Qt::DropActions supportedDropActions() const override; - QMimeData *mimeData(const QModelIndexList &indexes) const override; + [[nodiscard]] QMimeData *mimeData(const QModelIndexList &indexes) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; - QStringList mimeTypes() const override; + [[nodiscard]] QStringList mimeTypes() const override; void swapRows(int oldRow, int newRow); void toggleRow(int row, bool enable); void toggleRow(int row); @@ -92,8 +98,8 @@ public: explicit SetsDisplayModel(QObject *parent = nullptr); protected: - bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; void fetchMore(const QModelIndex &index) override; }; diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp new file mode 100644 index 000000000..4282c6dc8 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.cpp @@ -0,0 +1,19 @@ +#include "token_display_model.h" + +#include "../card_database_model.h" + +TokenDisplayModel::TokenDisplayModel(QObject *parent) : CardDatabaseDisplayModel(parent) +{ +} + +bool TokenDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const +{ + CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); + return info->getIsToken() && rowMatchesCardName(info); +} + +int TokenDisplayModel::rowCount(const QModelIndex &parent) const +{ + // always load all tokens at start + return QSortFilterProxyModel::rowCount(parent); +} \ No newline at end of file diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h new file mode 100644 index 000000000..f6b2fdfbb --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_display_model.h @@ -0,0 +1,23 @@ +/** + * @file token_display_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_TOKEN_DISPLAY_MODEL_H +#define COCKATRICE_TOKEN_DISPLAY_MODEL_H + +#include "../card_database_display_model.h" + +class TokenDisplayModel : public CardDatabaseDisplayModel +{ + Q_OBJECT +public: + explicit TokenDisplayModel(QObject *parent = nullptr); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // COCKATRICE_TOKEN_DISPLAY_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp new file mode 100644 index 000000000..89a1f71b1 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.cpp @@ -0,0 +1,22 @@ +#include "token_edit_model.h" + +#include "../card_database_display_model.h" +#include "../card_database_model.h" + +#include + +TokenEditModel::TokenEditModel(QObject *parent) : CardDatabaseDisplayModel(parent) +{ +} + +bool TokenEditModel::filterAcceptsRow(int sourceRow, const QModelIndex & /*sourceParent*/) const +{ + CardInfoPtr info = static_cast(sourceModel())->getCard(sourceRow); + return info->getIsToken() && info->getSets().contains(CardSet::TOKENS_SETNAME) && rowMatchesCardName(info); +} + +int TokenEditModel::rowCount(const QModelIndex &parent) const +{ + // always load all tokens at start + return QSortFilterProxyModel::rowCount(parent); +} \ No newline at end of file diff --git a/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h new file mode 100644 index 000000000..5e5843761 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/database/token/token_edit_model.h @@ -0,0 +1,23 @@ +/** + * @file token_edit_model.h + * @ingroup CardDatabaseModels + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_TOKEN_EDIT_MODEL_H +#define COCKATRICE_TOKEN_EDIT_MODEL_H + +#include "../card_database_display_model.h" + +class TokenEditModel : public CardDatabaseDisplayModel +{ + Q_OBJECT +public: + explicit TokenEditModel(QObject *parent = nullptr); + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +}; + +#endif // COCKATRICE_TOKEN_EDIT_MODEL_H diff --git a/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt new file mode 100644 index 000000000..851636a35 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/CMakeLists.txt @@ -0,0 +1,21 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS deck_list_model.h deck_list_sort_filter_proxy_model.h) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_models_deck_list STATIC ${MOC_SOURCES} deck_list_model.cpp deck_list_sort_filter_proxy_model.cpp +) + +target_include_directories(libcockatrice_models_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + libcockatrice_models_deck_list PUBLIC libcockatrice_card libcockatrice_deck_list ${QT_CORE_MODULE} +) diff --git a/cockatrice/src/deck/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp similarity index 53% rename from cockatrice/src/deck/deck_list_model.cpp rename to libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index 4ae041c5d..e43c612f0 100644 --- a/cockatrice/src/deck/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -1,41 +1,46 @@ #include "deck_list_model.h" -#include "../game/cards/card_database_manager.h" -#include "../main.h" -#include "../settings/cache_settings.h" -#include "deck_loader.h" - -#include -#include -#include -#include -#include -#include -#include -#include +#include DeckListModel::DeckListModel(QObject *parent) : QAbstractItemModel(parent), lastKnownColumn(1), lastKnownOrder(Qt::AscendingOrder) { - deckList = new DeckLoader; - deckList->setParent(this); - connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree); - connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged); + deckList = QSharedPointer(new DeckList()); root = new InnerDecklistNode; } +DeckListModel::DeckListModel(QObject *parent, const QSharedPointer &deckList) : DeckListModel(parent) +{ + setDeckList(deckList); + + // forward change signals + connect(this, &DeckListModel::cardAddedAt, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::cardRemoved, this, &DeckListModel::cardsChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardsChanged); + + connect(this, &DeckListModel::cardNodeAddedAt, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::cardNodeRemoved, this, &DeckListModel::cardNodesChanged); + connect(this, &DeckListModel::deckReplaced, this, &DeckListModel::cardNodesChanged); +} + DeckListModel::~DeckListModel() { delete root; } -QString DeckListModel::getGroupCriteriaForCard(CardInfoPtr info) const +/** + * @brief Extract the value from the card that is used for the group criteria. + * @param info Pointer to card information. + * @param criteria The group criteria + * @return String representing the value of the criteria. + */ +static QString extractGroupCriteriaValue(const CardInfoPtr &info, DeckListModelGroupCriteria::Type criteria) { if (!info) { return "unknown"; } - switch (activeGroupCriteria) { + switch (criteria) { case DeckListModelGroupCriteria::MAIN_TYPE: return info->getMainCardType(); case DeckListModelGroupCriteria::MANA_COST: @@ -52,7 +57,7 @@ void DeckListModel::rebuildTree() beginResetModel(); root->clearTree(); - InnerDecklistNode *listRoot = deckList->getRoot(); + InnerDecklistNode *listRoot = deckList->getTree()->getRoot(); for (int i = 0; i < listRoot->size(); i++) { auto *currentZone = dynamic_cast(listRoot->at(i)); @@ -66,8 +71,8 @@ void DeckListModel::rebuildTree() continue; } - CardInfoPtr info = CardDatabaseManager::getInstance()->getCardInfo(currentCard->getName()); - QString groupCriteria = getGroupCriteriaForCard(info); + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(currentCard->getName()); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); auto *groupNode = dynamic_cast(node->findChild(groupCriteria)); @@ -80,6 +85,8 @@ void DeckListModel::rebuildTree() } endResetModel(); + + refreshCardFormatLegalities(); } int DeckListModel::rowCount(const QModelIndex &parent) const @@ -109,77 +116,82 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const return {}; } - auto *temp = static_cast(index.internalPointer()); - auto *card = dynamic_cast(temp); - if (card == nullptr) { - const auto *node = dynamic_cast(temp); + auto *node = static_cast(index.internalPointer()); + auto *card = dynamic_cast(node); + + // Group node + if (!card) { + const auto *group = dynamic_cast(node); + switch (role) { - case Qt::FontRole: { - QFont f; - f.setBold(true); - return f; - } case Qt::DisplayRole: case Qt::EditRole: { switch (index.column()) { - case 0: - return node->recursiveCount(true); - case 1: { - if (role == Qt::DisplayRole) - return node->getVisibleName(); - return node->getName(); - } - case 2: { - return node->getCardSetShortName(); - } - case 3: { - return node->getCardCollectorNumber(); - } - case 4: { - return node->getCardProviderId(); - } + case DeckListModelColumns::CARD_AMOUNT: + return group->recursiveCount(true); + case DeckListModelColumns::CARD_NAME: + if (role == Qt::DisplayRole) { + return group->getVisibleName(); + } + return group->getName(); + case DeckListModelColumns::CARD_SET: + return group->getCardSetShortName(); + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + return group->getCardCollectorNumber(); + case DeckListModelColumns::CARD_PROVIDER_ID: + return group->getCardProviderId(); default: return {}; } } - case Qt::BackgroundRole: { - int color = 90 + 60 * node->depth(); - return QBrush(QColor(color, 255, color)); - } - case Qt::ForegroundRole: { - return QBrush(QColor(0, 0, 0)); - } + case DeckRoles::IsCardRole: + return false; + + case DeckRoles::DepthRole: + return group->depth(); + + // legality does not apply to group nodes + case DeckRoles::IsLegalRole: + return true; + default: return {}; } - } else { - switch (role) { - case Qt::DisplayRole: - case Qt::EditRole: { - switch (index.column()) { - case 0: - return card->getNumber(); - case 1: - return card->getName(); - case 2: - return card->getCardSetShortName(); - case 3: - return card->getCardCollectorNumber(); - case 4: - return card->getCardProviderId(); - default: - return {}; - } + } + + // Card node + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + switch (index.column()) { + case DeckListModelColumns::CARD_AMOUNT: + return card->getNumber(); + case DeckListModelColumns::CARD_NAME: + return card->getName(); + case DeckListModelColumns::CARD_SET: + return card->getCardSetShortName(); + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: + return card->getCardCollectorNumber(); + case DeckListModelColumns::CARD_PROVIDER_ID: + return card->getCardProviderId(); + default: + return {}; } - case Qt::BackgroundRole: { - int color = 255 - (index.row() % 2) * 30; - return QBrush(QColor(color, color, color)); - } - case Qt::ForegroundRole: { - return QBrush(QColor(0, 0, 0)); - } - default: - return {}; + + case DeckRoles::IsCardRole: { + return true; + } + + case DeckRoles::DepthRole: { + return card->depth(); + } + + case DeckRoles::IsLegalRole: { + return card->getFormatLegality(); + } + + default: { + return {}; } } } @@ -195,15 +207,15 @@ QVariant DeckListModel::headerData(const int section, const Qt::Orientation orie } switch (section) { - case 0: + case DeckListModelColumns::CARD_AMOUNT: return tr("Count"); - case 1: + case DeckListModelColumns::CARD_NAME: return tr("Card"); - case 2: + case DeckListModelColumns::CARD_SET: return tr("Set"); - case 3: + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: return tr("Number"); - case 4: + case DeckListModelColumns::CARD_PROVIDER_ID: return tr("Provider ID"); default: return {}; @@ -242,6 +254,22 @@ Qt::ItemFlags DeckListModel::flags(const QModelIndex &index) const return result; } +void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent) +{ + int rows = rowCount(parent); + if (rows == 0) + return; + + QModelIndex topLeft = index(0, 0, parent); + QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent); + emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole}); + + for (int r = 0; r < rows; ++r) { + QModelIndex child = index(r, 0, parent); + emitBackgroundUpdates(child); + } +} + void DeckListModel::emitRecursiveUpdates(const QModelIndex &index) { if (!index.isValid()) { @@ -260,19 +288,20 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con } switch (index.column()) { - case 0: + case DeckListModelColumns::CARD_AMOUNT: node->setNumber(value.toInt()); + refreshCardFormatLegalities(); break; - case 1: + case DeckListModelColumns::CARD_NAME: node->setName(value.toString()); break; - case 2: + case DeckListModelColumns::CARD_SET: node->setCardSetShortName(value.toString()); break; - case 3: + case DeckListModelColumns::CARD_COLLECTOR_NUMBER: node->setCardCollectorNumber(value.toString()); break; - case 4: + case DeckListModelColumns::CARD_PROVIDER_ID: node->setCardProviderId(value.toString()); break; default: @@ -281,8 +310,8 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con emitRecursiveUpdates(index); deckList->refreshDeckHash(); + emit deckHashChanged(); - emit dataChanged(index, index); return true; } @@ -301,7 +330,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) for (int i = 0; i < count; i++) { AbstractDecklistNode *toDelete = node->takeAt(row); if (auto *temp = dynamic_cast(toDelete)) { - deckList->deleteNode(temp->getDataNode()); + deckList->getTree()->deleteNode(temp->getDataNode()); } delete toDelete; } @@ -313,6 +342,9 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent) emitRecursiveUpdates(parent); } + deckList->refreshDeckHash(); + emit deckHashChanged(); + return true; } @@ -337,12 +369,12 @@ DecklistModelCardNode *DeckListModel::findCardNode(const QString &cardName, return nullptr; } - CardInfoPtr info = CardDatabaseManager::getInstance()->getCardInfo(cardName); + CardInfoPtr info = CardDatabaseManager::query()->getCardInfo(cardName); if (!info) { return nullptr; } - QString groupCriteria = getGroupCriteriaForCard(info); + QString groupCriteria = extractGroupCriteriaValue(info, activeGroupCriteria); InnerDecklistNode *groupNode = dynamic_cast(zoneNode->findChild(groupCriteria)); if (!groupNode) { return nullptr; @@ -367,7 +399,7 @@ QModelIndex DeckListModel::findCard(const QString &cardName, QModelIndex DeckListModel::addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway) { - ExactCard card = CardDatabaseManager::getInstance()->getCard({cardName}); + ExactCard card = CardDatabaseManager::query()->getCard({cardName}); if (!card) { if (abAddAnyway) { @@ -395,7 +427,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam CardInfoPtr cardInfo = card.getCardPtr(); PrintingInfo printingInfo = card.getPrinting(); - QString groupCriteria = getGroupCriteriaForCard(cardInfo); + QString groupCriteria = extractGroupCriteriaValue(cardInfo, activeGroupCriteria); InnerDecklistNode *groupNode = createNodeIfNeeded(groupCriteria, zoneNode); const QModelIndex parentIndex = nodeToIndex(groupNode); @@ -403,6 +435,7 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam card.getName(), printingInfo.getUuid(), printingInfo.getProperty("num"))); const auto cardSetName = printingInfo.getSet().isNull() ? "" : printingInfo.getSet()->getCorrectedShortName(); + bool cardNodeAdded = false; if (!cardNode) { // Determine the correct index int insertRow = findSortedInsertRow(groupNode, cardInfo); @@ -413,19 +446,92 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam beginInsertRows(parentIndex, insertRow, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); endInsertRows(); + + cardNodeAdded = true; } else { cardNode->setNumber(cardNode->getNumber() + 1); cardNode->setCardSetShortName(cardSetName); cardNode->setCardCollectorNumber(printingInfo.getProperty("num")); cardNode->setCardProviderId(printingInfo.getProperty("uuid")); - deckList->refreshDeckHash(); + // Emit dataChanged for the amount column since we modified it + QModelIndex cardIndex = nodeToIndex(cardNode); + QModelIndex amountIndex = cardIndex.sibling(cardIndex.row(), DeckListModelColumns::CARD_AMOUNT); + emit dataChanged(amountIndex, amountIndex, {Qt::EditRole}); } sort(lastKnownColumn, lastKnownOrder); + refreshCardFormatLegalities(); emitRecursiveUpdates(parentIndex); - return nodeToIndex(cardNode); + + deckList->refreshDeckHash(); + emit deckHashChanged(); + + auto index = nodeToIndex(cardNode); + + if (cardNodeAdded) { + emit cardNodeAddedAt(index); + } + + emit cardAddedAt(index); + + return index; } -int DeckListModel::findSortedInsertRow(InnerDecklistNode *parent, CardInfoPtr cardInfo) const +bool DeckListModel::offsetCountAtIndex(const QModelIndex &idx, int offset) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + const QModelIndex numberIndex = idx.siblingAtColumn(DeckListModelColumns::CARD_AMOUNT); + const int count = numberIndex.data(Qt::EditRole).toInt(); + const int newCount = count + offset; + + if (newCount <= 0) { + removeRow(idx.row(), idx.parent()); + emit cardNodeRemoved(); + } else { + setData(numberIndex, newCount, Qt::EditRole); + } + + if (offset > 0) { + emit cardAddedAt(idx); + } else if (offset < 0) { + emit cardRemoved(); + } + + return true; +} + +bool DeckListModel::removeCardAtIndex(const QModelIndex &idx) +{ + if (!idx.isValid()) { + return false; + } + + auto *node = static_cast(idx.internalPointer()); + auto *card = dynamic_cast(node); + + if (!card) { + return false; + } + + bool success = removeRow(idx.row(), idx.parent()); + + if (success) { + emit cardRemoved(); + } + + return success; +} + +int DeckListModel::findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const { if (!cardInfo) { return parent->size(); // fallback: append at end @@ -518,206 +624,153 @@ void DeckListModel::sort(int column, Qt::SortOrder order) emit layoutChanged(); } -void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria newCriteria) +void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria) { activeGroupCriteria = newCriteria; rebuildTree(); } +void DeckListModel::setActiveFormat(const QString &_format) +{ + deckList->setGameFormat(_format); + refreshCardFormatLegalities(); + emitBackgroundUpdates(QModelIndex()); // start from root +} + void DeckListModel::cleanList() { - setDeckList(new DeckLoader); + setDeckList(QSharedPointer(new DeckList())); } /** - * @param _deck The deck. Takes ownership of the object + * @param _deck The deck. */ -void DeckListModel::setDeckList(DeckLoader *_deck) +void DeckListModel::setDeckList(const QSharedPointer &_deck) { - deckList->deleteLater(); - deckList = _deck; - deckList->setParent(this); - connect(deckList, &DeckLoader::deckLoaded, this, &DeckListModel::rebuildTree); - connect(deckList, &DeckLoader::deckHashChanged, this, &DeckListModel::deckHashChanged); + if (deckList != _deck) { + deckList = _deck; + } rebuildTree(); + emit deckReplaced(); } -QList DeckListModel::getCards() const +void DeckListModel::forEachCard(const std::function &func) { - QList cards; - DeckList *decklist = getDeckList(); - if (!decklist) { - return cards; - } - InnerDecklistNode *listRoot = decklist->getRoot(); - if (!listRoot) - return cards; - - for (int i = 0; i < listRoot->size(); i++) { - InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); - if (!currentZone) - continue; - for (int j = 0; j < currentZone->size(); j++) { - DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); - if (!currentCard) - continue; - for (int k = 0; k < currentCard->getNumber(); ++k) { - ExactCard card = CardDatabaseManager::getInstance()->getCard(currentCard->toCardRef()); - if (card) { - cards.append(card); - } else { - qDebug() << "Card not found in database!"; - } - } - } - } - return cards; + deckList->forEachCard(func); } -QList DeckListModel::getCardsForZone(const QString &zoneName) const +QList DeckListModel::getCardNodes() const { - QList cards; - DeckList *decklist = getDeckList(); - if (!decklist) { - return cards; - } - InnerDecklistNode *listRoot = decklist->getRoot(); - if (!listRoot) - return cards; - - for (int i = 0; i < listRoot->size(); i++) { - InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); - if (!currentZone) - continue; - if (currentZone->getName() == zoneName) { - for (int j = 0; j < currentZone->size(); j++) { - DecklistCardNode *currentCard = dynamic_cast(currentZone->at(j)); - if (!currentCard) - continue; - for (int k = 0; k < currentCard->getNumber(); ++k) { - ExactCard card = CardDatabaseManager::getInstance()->getCard(currentCard->toCardRef()); - if (card) { - cards.append(card); - } else { - qDebug() << "Card not found in database!"; - } - } - } - } - } - return cards; + return deckList->getCardNodes(); } -QList *DeckListModel::getZones() const +QList DeckListModel::getCardNodesForZone(const QString &zoneName) const { - QList *zones = new QList(); - DeckList *decklist = getDeckList(); - if (!decklist) { - return zones; - } - InnerDecklistNode *listRoot = decklist->getRoot(); - if (!listRoot) - return zones; + return deckList->getCardNodes({zoneName}); +} + +QList DeckListModel::getCardNames() const +{ + auto nodes = deckList->getCardNodes(); + + QList names; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(names), [](auto node) { return node->getName(); }); + + return names; +} + +QList DeckListModel::getCardRefs() const +{ + auto nodes = deckList->getCardNodes(); + + QList cardRefs; + std::transform(nodes.cbegin(), nodes.cend(), std::back_inserter(cardRefs), + [](auto node) { return node->toCardRef(); }); + + return cardRefs; +} + +QList DeckListModel::getZones() const +{ + auto zoneNodes = deckList->getZoneNodes(); + + QList zones; + std::transform(zoneNodes.cbegin(), zoneNodes.cend(), std::back_inserter(zones), + [](auto zoneNode) { return zoneNode->getName(); }); - for (int i = 0; i < listRoot->size(); i++) { - InnerDecklistNode *currentZone = dynamic_cast(listRoot->at(i)); - if (!currentZone) - continue; - zones->append(currentZone->getName()); - } return zones; } -void DeckListModel::printDeckListNode(QTextCursor *cursor, InnerDecklistNode *node) +static int maxAllowedForLegality(const FormatRules &format, const QString &legality) { - const int totalColumns = 2; - - if (node->height() == 1) { - QTextBlockFormat blockFormat; - QTextCharFormat charFormat; - charFormat.setFontPointSize(11); - charFormat.setFontWeight(QFont::Bold); - cursor->insertBlock(blockFormat, charFormat); - - QTextTableFormat tableFormat; - tableFormat.setCellPadding(0); - tableFormat.setCellSpacing(0); - tableFormat.setBorder(0); - QTextTable *table = cursor->insertTable(node->size() + 1, totalColumns, tableFormat); - for (int i = 0; i < node->size(); i++) { - auto *card = dynamic_cast(node->at(i)); - - QTextCharFormat cellCharFormat; - cellCharFormat.setFontPointSize(9); - - QTextTableCell cell = table->cellAt(i, 0); - cell.setFormat(cellCharFormat); - QTextCursor cellCursor = cell.firstCursorPosition(); - cellCursor.insertText(QString("%1 ").arg(card->getNumber())); - - cell = table->cellAt(i, 1); - cell.setFormat(cellCharFormat); - cellCursor = cell.firstCursorPosition(); - cellCursor.insertText(card->getName()); - } - } else if (node->height() == 2) { - QTextBlockFormat blockFormat; - QTextCharFormat charFormat; - charFormat.setFontPointSize(14); - charFormat.setFontWeight(QFont::Bold); - - cursor->insertBlock(blockFormat, charFormat); - - QTextTableFormat tableFormat; - tableFormat.setCellPadding(10); - tableFormat.setCellSpacing(0); - tableFormat.setBorder(0); - QVector constraints; - for (int i = 0; i < totalColumns; i++) { - constraints << QTextLength(QTextLength::PercentageLength, 100.0 / totalColumns); - } - tableFormat.setColumnWidthConstraints(constraints); - - QTextTable *table = cursor->insertTable(1, totalColumns, tableFormat); - for (int i = 0; i < node->size(); i++) { - QTextCursor cellCursor = table->cellAt(0, (i * totalColumns) / node->size()).lastCursorPosition(); - printDeckListNode(&cellCursor, dynamic_cast(node->at(i))); + for (const AllowedCount &c : format.allowedCounts) { + if (c.label == legality) { + return c.max; } } - - cursor->movePosition(QTextCursor::End); + return -1; // unknown legality → treat as illegal } -void DeckListModel::printDeckList(QPrinter *printer) +static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity) { - QTextDocument doc; - - QFont font("Serif"); - font.setStyleHint(QFont::Serif); - doc.setDefaultFont(font); - - QTextCursor cursor(&doc); - - QTextBlockFormat headerBlockFormat; - QTextCharFormat headerCharFormat; - headerCharFormat.setFontPointSize(16); - headerCharFormat.setFontWeight(QFont::Bold); - - cursor.insertBlock(headerBlockFormat, headerCharFormat); - cursor.insertText(deckList->getName()); - - headerCharFormat.setFontPointSize(12); - cursor.insertBlock(headerBlockFormat, headerCharFormat); - cursor.insertText(deckList->getComments()); - cursor.insertBlock(headerBlockFormat, headerCharFormat); - - for (int i = 0; i < root->size(); i++) { - cursor.insertHtml("
"); - // cursor.insertHtml("
"); - cursor.insertBlock(headerBlockFormat, headerCharFormat); - - printDeckListNode(&cursor, dynamic_cast(root->at(i))); + if (format.isEmpty()) { + return true; } - doc.print(printer); + auto formatRules = CardDatabaseManager::query()->getFormat(format); + + // if format has no custom rules, then just do the default check + if (!formatRules) { + return cardInfo.isLegalInFormat(format); + } + + // Exceptions always win + if (cardHasAnyException(cardInfo, *formatRules)) { + return true; + } + + // check legality prop + const QString legality = cardInfo.getLegalityProp(format); + if (legality.isEmpty()) { + return false; + } + + int maxAllowed = maxAllowedForLegality(*formatRules, legality); + + if (maxAllowed == -1) { + return false; + } + + if (maxAllowed < 0) { // unlimited + return true; + } + + return quantity <= maxAllowed; +} + +static bool isCardNodeLegalForFormat(const QString &format, const InnerDecklistNode *zone, const DecklistCardNode *card) +{ + // Don't check legality for tokens + if (zone->getName() == DECK_ZONE_TOKENS) { + return true; + } + + // unknown cards are not legal + ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef()); + if (!exactCard) { + return false; + } + + // actual check + return isCardQuantityLegalForFormat(format, exactCard.getInfo(), card->getNumber()); +} + +void DeckListModel::refreshCardFormatLegalities() +{ + QString format = deckList->getGameFormat(); + + deckList->forEachCard([&format](const InnerDecklistNode *zone, DecklistCardNode *card) { + bool legal = isCardNodeLegalForFormat(format, zone, card); + card->setFormatLegality(legal); + }); } diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h new file mode 100644 index 000000000..86d36b7f9 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -0,0 +1,438 @@ +#ifndef DECKLISTMODEL_H +#define DECKLISTMODEL_H + +#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h> +#include <../../../../libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h> +#include +#include +#include +#include + +class CardDatabase; +class QPrinter; +class QTextCursor; + +/** + * @namespace DeckRoles + * @brief Custom model roles used by the DeckListModel for data retrieval. + * + * These roles extend Qt's item data roles starting at Qt::UserRole. + */ +namespace DeckRoles +{ +/** + * @enum DeckRoles + * @brief Custom data roles for deck-related model items. + * + * These roles are used to retrieve specialized data from the DeckListModel. + */ +enum +{ + IsCardRole = Qt::UserRole + 1, /**< Indicates whether the item represents a card. */ + DepthRole, /**< Depth level within the deck's grouping hierarchy. */ + IsLegalRole /**< Whether the card is legal in the current deck format. */ +}; +} // namespace DeckRoles + +/** + * @namespace DeckListModelColumns + * @brief Column indices for the DeckListModel. + * + * These values map to the columns in the deck list table representation. + */ +namespace DeckListModelColumns +{ +/** + * @enum DeckListModelColumns + * @brief Column identifiers for displaying card information in the deck list. + */ +enum +{ + CARD_AMOUNT = 0, /**< The number of copies of the card. */ + CARD_NAME = 1, /**< The card's name. */ + CARD_SET = 2, /**< The set or expansion the card belongs to. */ + CARD_COLLECTOR_NUMBER = 3, /**< Collector number of the card within the set. */ + CARD_PROVIDER_ID = 4 /**< ID used by the external data provider (e.g., Scryfall). */ +}; +} // namespace DeckListModelColumns + +/** + * @namespace DeckListModelGroupCriteria + * @brief Specifies criteria used to group cards in the DeckListModel. + * + * These values determine how cards are grouped in UI views such as the deck editor. + */ +namespace DeckListModelGroupCriteria +{ +/** + * @enum DeckListModelGroupCriteria + * @brief Available grouping strategies for deck visualization. + */ +enum Type +{ + MAIN_TYPE, /**< Group cards by their main type (e.g., creature, instant). */ + MANA_COST, /**< Group cards by their total mana cost. */ + COLOR /**< Group cards by their color identity. */ +}; +static inline QString toString(Type t) +{ + switch (t) { + case MAIN_TYPE: + return "Main Type"; + case MANA_COST: + return "Mana Cost"; + case COLOR: + return "Colors"; + } + return {}; +} + +static inline Type fromString(const QString &s) +{ + if (s == "Main Type") + return MAIN_TYPE; + if (s == "Mana Cost") + return MANA_COST; + if (s == "Colors") + return COLOR; + return MAIN_TYPE; // default +} +} // namespace DeckListModelGroupCriteria + +/** + * @class DecklistModelCardNode + * @ingroup DeckModels + * @brief Adapter node that wraps a DecklistCardNode for use in the DeckListModel tree. + * + * This class forwards all property accessors (name, number, provider ID, set info, etc.) + * to the underlying DecklistCardNode. It exists so the model can represent cards + * in the same hierarchy as InnerDecklistNode containers. + */ +class DecklistModelCardNode : public AbstractDecklistCardNode +{ +private: + DecklistCardNode *dataNode; /**< Pointer to the underlying data node. */ + +public: + /** + * @brief Constructs a model node wrapping a DecklistCardNode. + * @param _dataNode The underlying DecklistCardNode to wrap. + * @param _parent The parent InnerDecklistNode in the model tree. + * @param position Optional position to insert in parent (-1 appends at end). + */ + DecklistModelCardNode(DecklistCardNode *_dataNode, InnerDecklistNode *_parent, int position = -1) + : AbstractDecklistCardNode(_parent, position), dataNode(_dataNode) + { + } + [[nodiscard]] int getNumber() const override + { + return dataNode->getNumber(); + } + void setNumber(int _number) override + { + dataNode->setNumber(_number); + } + [[nodiscard]] QString getName() const override + { + return dataNode->getName(); + } + void setName(const QString &_name) override + { + dataNode->setName(_name); + } + [[nodiscard]] QString getCardProviderId() const override + { + return dataNode->getCardProviderId(); + } + void setCardProviderId(const QString &_cardProviderId) override + { + dataNode->setCardProviderId(_cardProviderId); + } + [[nodiscard]] QString getCardSetShortName() const override + { + return dataNode->getCardSetShortName(); + } + void setCardSetShortName(const QString &_cardSetShortName) override + { + dataNode->setCardSetShortName(_cardSetShortName); + } + [[nodiscard]] QString getCardCollectorNumber() const override + { + return dataNode->getCardCollectorNumber(); + } + void setCardCollectorNumber(const QString &_cardSetNumber) override + { + dataNode->setCardCollectorNumber(_cardSetNumber); + } + bool getFormatLegality() const override + { + return dataNode->getFormatLegality(); + } + void setFormatLegality(const bool _formatLegal) override + { + dataNode->setFormatLegality(_formatLegal); + } + + /** + * @brief Returns the underlying data node. + * @return Pointer to the DecklistCardNode wrapped by this node. + */ + [[nodiscard]] DecklistCardNode *getDataNode() const + { + return dataNode; + } + [[nodiscard]] bool isDeckHeader() const override + { + return false; + } +}; + +/** + * @class DeckListModel + * @ingroup DeckModels + * @brief Qt model representing a decklist for use in views (tree/table). + * + * DeckListModel is a QAbstractItemModel that exposes the structure of a deck + * (zones and cards) to Qt views. It organizes cards hierarchically under + * InnerDecklistNode containers and supports grouping, sorting, adding/removing + * cards, and printing decklists. + * + * Outside code should refrain from modifying the model with methods inherited from QAbstractItemModel, such as with + * `setData` or `removeRow`. + * Instead, use the custom methods on this class to modify the model, such as `addCard`, `offsetCountAtIndex`, or + * `removeCardAtIndex`. + * This ensures the custom signals for this class are correctly emitted. + * + * Signals: + * - deckHashChanged(): emitted when the deck contents change in a way that + * affects its hash. + * + * Slots: + * - rebuildTree(): rebuilds the model structure from the underlying node tree. + */ +class DeckListModel : public QAbstractItemModel +{ + Q_OBJECT + +public slots: + /** + * @brief Rebuilds the model tree from the underlying node tree. + * + * This updates all indices and ensures the model reflects the current + * state of the deck. + */ + void rebuildTree(); + + /** + * @brief Sets the criteria used to group cards in the model. + * @param newCriteria The new grouping criteria. + */ + void setActiveGroupCriteria(DeckListModelGroupCriteria::Type newCriteria); + + void setActiveFormat(const QString &_format); + +signals: + /** + * @brief Emitted whenever the deck hash changes due to modifications in the model. + */ + void deckHashChanged(); + + /** + * @brief Emitted whenever the cards in the deck changes. This includes when the deck is replaced. + */ + void cardsChanged(); + + /** + * @brief Emitted whenever a card is added to the deck, regardless of whether it's an entirely new card or an + * existing card that got incremented. + * @param index The index of the card that got added. + */ + void cardAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever a card is removed from the deck, regardless of whether a card node was removed or an + * existing card got decremented. + */ + void cardRemoved(); + + /** + * @brief Emitted whenever a card node is added or removed. This includes when the deck is replaced. + */ + void cardNodesChanged(); + + /** + * @brief Emitted whenever a new card node is added. + * @param index The index of the card node that got added. + */ + void cardNodeAddedAt(const QModelIndex &index); + + /** + * @brief Emitted whenever a card node is removed. + */ + void cardNodeRemoved(); + + /** + * @brief Emitted whenever the deck in the model has been replaced with a new one + */ + void deckReplaced(); + +public: + explicit DeckListModel(QObject *parent = nullptr); + explicit DeckListModel(QObject *parent, const QSharedPointer &deckList); + ~DeckListModel() override; + + /** + * @brief Returns the root index of the model. + * @return QModelIndex representing the root node. + */ + [[nodiscard]] QModelIndex getRoot() const + { + return nodeToIndex(root); + } + + /// @name Qt model overrides + ///@{ + [[nodiscard]] int rowCount(const QModelIndex &parent) const override; + [[nodiscard]] int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + [[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent) const override; + [[nodiscard]] QModelIndex parent(const QModelIndex &index) const override; + [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + void sort(int column, Qt::SortOrder order) override; + ///@} + + /** + * @brief Finds a card by name, zone, and optional identifiers. + * @param cardName The card's name. + * @param zoneName The zone to search in (main/side/etc.). + * @param providerId Optional provider-specific ID. + * @param cardNumber Optional collector number. + * @return QModelIndex of the card, or invalid index if not found. + */ + [[nodiscard]] QModelIndex findCard(const QString &cardName, + const QString &zoneName, + const QString &providerId = "", + const QString &cardNumber = "") const; + + /** + * @brief Adds a card using the preferred printing if available. + * + * @param cardName Name of the card to add. + * @param zoneName Zone to insert the card into. + * @param abAddAnyway Whether to add the card even if resolution fails. + * @return QModelIndex pointing to the newly inserted card node. + */ + QModelIndex addPreferredPrintingCard(const QString &cardName, const QString &zoneName, bool abAddAnyway); + + /** + * @brief Adds an ExactCard to the specified zone. + * @param card The card to add. + * @param zoneName The zone to insert the card into. + * @return QModelIndex pointing to the newly inserted card node. + */ + QModelIndex addCard(const ExactCard &card, const QString &zoneName); + + /** + * @brief Changes the `amount` field in the card node at the index by the amount. + * Removes the node if it causes the amount to fall to 0 or below. + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @param offset The amount to change the amount field by. + * @return Whether the operation was successful + */ + bool offsetCountAtIndex(const QModelIndex &idx, int offset); + + /** + * @brief Removes the card node at the index + * @param idx The index of a card node. No-ops if the index is invalid or not a card node. + * @return Whether the node was removed. + */ + bool removeCardAtIndex(const QModelIndex &idx); + + /** + * @brief Removes all cards and resets the model. + */ + void cleanList(); + + [[nodiscard]] QSharedPointer getDeckList() const + { + return deckList; + } + void setDeckList(const QSharedPointer &_deck); + + /** + * @brief Apply a function to every card in the deck tree. + * + * @param func Function taking (zone node, card node). + */ + void forEachCard(const std::function &func); + + /** + * @brief Gets a list of all card nodes in the deck. + */ + [[nodiscard]] QList getCardNodes() const; + [[nodiscard]] QList getCardNodesForZone(const QString &zoneName) const; + + /** + * @brief Gets a deduplicated list of all card names that appear in the model + */ + [[nodiscard]] QList getCardNames() const; + /** + * @brief Gets a deduplicated list of all CardRefs that appear in the model + */ + [[nodiscard]] QList getCardRefs() const; + /** + * @brief Gets a list of all zone names that appear in the model + */ + [[nodiscard]] QList getZones() const; + +private: + QSharedPointer deckList; /**< Pointer to the decklist providing the underlying data. */ + InnerDecklistNode *root; /**< Root node of the model tree. */ + DeckListModelGroupCriteria::Type activeGroupCriteria = DeckListModelGroupCriteria::MAIN_TYPE; + int lastKnownColumn; /**< Last column used for sorting. */ + Qt::SortOrder lastKnownOrder; /**< Last known sort order. */ + + InnerDecklistNode *createNodeIfNeeded(const QString &name, InnerDecklistNode *parent); + QModelIndex nodeToIndex(AbstractDecklistNode *node) const; + [[nodiscard]] DecklistModelCardNode *findCardNode(const QString &cardName, + const QString &zoneName, + const QString &providerId = "", + const QString &cardNumber = "") const; + + /** + * @brief Determines the sorted insertion row for a card. + * @param parent The parent node where the card will be inserted. + * @param cardInfo The card info to insert. + * @return Row index where the card should be inserted to maintain sort order. + */ + int findSortedInsertRow(const InnerDecklistNode *parent, const CardInfoPtr &cardInfo) const; + + /** + * @brief Recursively emits the dataChanged signal with role as Qt::BackgroundRole for all indices that are children + * of the given node. This is used to update the background color when changing formats. + * @param parent The parent node + */ + void emitBackgroundUpdates(const QModelIndex &parent); + + /** + * @brief Recursively emits the dataChanged signal for the given node and all parent nodes. + * @param index The parent node + */ + void emitRecursiveUpdates(const QModelIndex &index); + + void sortHelper(InnerDecklistNode *node, Qt::SortOrder order); + + template T getNode(const QModelIndex &index) const + { + if (!index.isValid()) + return dynamic_cast(root); + return dynamic_cast(static_cast(index.internalPointer())); + } + + void refreshCardFormatLegalities(); +}; + +#endif diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp new file mode 100644 index 000000000..0ec159737 --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.cpp @@ -0,0 +1,55 @@ +#include "deck_list_sort_filter_proxy_model.h" + +#include "deck_list_model.h" + +bool DeckListSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + auto *src = sourceModel(); + + // Inner nodes? -> sort alphabetically by column 1 + bool leftIsCard = src->data(left, Qt::UserRole + 1).toBool(); + bool rightIsCard = src->data(right, Qt::UserRole + 1).toBool(); + + if (!leftIsCard || !rightIsCard) { + QString lName = src->data(left.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); + QString rName = src->data(right.siblingAtColumn(DeckListModelColumns::CARD_NAME), Qt::EditRole).toString(); + return lName.localeAwareCompare(rName) < 0; + } + + // Both are cards -> apply sort criteria + auto *lNode = static_cast(left.internalPointer()); + auto *rNode = static_cast(right.internalPointer()); + + CardInfoPtr lInfo = CardDatabaseManager::query()->guessCard({lNode->getName()}).getCardPtr(); + CardInfoPtr rInfo = CardDatabaseManager::query()->guessCard({rNode->getName()}).getCardPtr(); + + // Example: multiple tie-break criteria (colors > cmc > name) + for (const QString &crit : sortCriteria) { + if (crit == "name") { + QString ln = lNode->getName(); + QString rn = rNode->getName(); + int cmp = ln.localeAwareCompare(rn); + if (cmp != 0) + return cmp < 0; + } else if (crit == "cmc") { + int lc = lInfo ? lInfo->getCmc().toInt() : 0; + int rc = rInfo ? rInfo->getCmc().toInt() : 0; + if (lc != rc) + return lc < rc; + } else if (crit == "colors") { + QString lr = lInfo ? lInfo->getColors() : QString(); + QString rr = rInfo ? rInfo->getColors() : QString(); + int cmp = lr.localeAwareCompare(rr); + if (cmp != 0) + return cmp < 0; + } else if (crit == "maintype") { + QString lr = lInfo ? lInfo->getMainCardType() : QString(); + QString rr = rInfo ? rInfo->getMainCardType() : QString(); + int cmp = lr.localeAwareCompare(rr); + if (cmp != 0) + return cmp < 0; + } + } + + return false; +} diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h new file mode 100644 index 000000000..94742795d --- /dev/null +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_sort_filter_proxy_model.h @@ -0,0 +1,34 @@ +/** + * @file deck_list_sort_filter_proxy_model.h + * @ingroup DeckEditorCardGroupWidgets + * @brief TODO: Document this. + */ + +#ifndef COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H +#define COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H + +#include +#include + +class DeckListSortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit DeckListSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) + { + } + + void setSortCriteria(const QStringList &criteria) + { + sortCriteria = criteria; + invalidate(); // re-sort + } + +protected: + [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QStringList sortCriteria; +}; + +#endif // COCKATRICE_DECK_LIST_SORT_FILTER_PROXY_MODEL_H diff --git a/libcockatrice_network/CMakeLists.txt b/libcockatrice_network/CMakeLists.txt new file mode 100644 index 000000000..3069c0db4 --- /dev/null +++ b/libcockatrice_network/CMakeLists.txt @@ -0,0 +1,14 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(libcockatrice/network/client) +add_subdirectory(libcockatrice/network/server) + +add_library(libcockatrice_network INTERFACE) + +target_include_directories(libcockatrice_network INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + libcockatrice_network INTERFACE ${COCKATRICE_QT_MODULES} libcockatrice_network_client libcockatrice_network_server +) diff --git a/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt new file mode 100644 index 000000000..bcf62463c --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/CMakeLists.txt @@ -0,0 +1,18 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(abstract) +add_subdirectory(local) +add_subdirectory(remote) + +add_library(libcockatrice_network_client INTERFACE) + +target_include_directories(libcockatrice_network_client INTERFACE .) + +target_link_libraries( + libcockatrice_network_client + INTERFACE ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract libcockatrice_network_client_local + libcockatrice_network_client_remote +) diff --git a/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt new file mode 100644 index 000000000..2753246de --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/abstract/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS abstract_client.h) + +set(SOURCES abstract_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_abstract STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_abstract libcockatrice_protocol libcockatrice_network_server_remote) + +target_include_directories(libcockatrice_network_client_abstract PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_abstract + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets libcockatrice_protocol + libcockatrice_network_server_remote +) diff --git a/cockatrice/src/server/abstract_client.cpp b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp similarity index 87% rename from cockatrice/src/server/abstract_client.cpp rename to libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp index 4f3098871..11458768d 100644 --- a/cockatrice/src/server/abstract_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.cpp @@ -1,25 +1,24 @@ #include "abstract_client.h" -#include "featureset.h" -#include "get_pb_extension.h" -#include "pb/commands.pb.h" -#include "pb/event_add_to_list.pb.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_list_rooms.pb.h" -#include "pb/event_notify_user.pb.h" -#include "pb/event_remove_from_list.pb.h" -#include "pb/event_replay_added.pb.h" -#include "pb/event_server_identification.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_server_shutdown.pb.h" -#include "pb/event_user_joined.pb.h" -#include "pb/event_user_left.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/server_message.pb.h" -#include "pending_command.h" - #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include AbstractClient::AbstractClient(QObject *parent) : QObject(parent), nextCmdId(0), status(StatusDisconnected), serverSupportsPasswordHash(false) diff --git a/cockatrice/src/server/abstract_client.h b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h similarity index 95% rename from cockatrice/src/server/abstract_client.h rename to libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h index 5e4948f5e..dc3be5a94 100644 --- a/cockatrice/src/server/abstract_client.h +++ b/libcockatrice_network/libcockatrice/network/client/abstract/abstract_client.h @@ -1,12 +1,16 @@ +/** + * @file abstract_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + #ifndef ABSTRACTCLIENT_H #define ABSTRACTCLIENT_H -#include "pb/response.pb.h" -#include "pb/serverinfo_user.pb.h" - #include -#include #include +#include +#include class PendingCommand; class CommandContainer; diff --git a/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt new file mode 100644 index 000000000..d12a324dc --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/local/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS local_client.h) + +set(SOURCES local_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_local STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_local libcockatrice_network_client_abstract) + +target_include_directories(libcockatrice_network_client_local PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_local + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract +) diff --git a/cockatrice/src/server/local_client.cpp b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp similarity index 86% rename from cockatrice/src/server/local_client.cpp rename to libcockatrice_network/libcockatrice/network/client/local/local_client.cpp index 734660af1..eefa3a2f3 100644 --- a/cockatrice/src/server/local_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.cpp @@ -1,8 +1,9 @@ #include "local_client.h" -#include "debug_pb_message.h" -#include "local_server_interface.h" -#include "pb/session_commands.pb.h" +#include "../../server/local/local_server_interface.h" + +#include +#include LocalClient::LocalClient(LocalServerInterface *_lsi, const QString &_playerName, diff --git a/cockatrice/src/server/local_client.h b/libcockatrice_network/libcockatrice/network/client/local/local_client.h similarity index 83% rename from cockatrice/src/server/local_client.h rename to libcockatrice_network/libcockatrice/network/client/local/local_client.h index 145887ea8..e8c5330ac 100644 --- a/cockatrice/src/server/local_client.h +++ b/libcockatrice_network/libcockatrice/network/client/local/local_client.h @@ -1,7 +1,13 @@ +/** + * @file local_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + #ifndef LOCALCLIENT_H #define LOCALCLIENT_H -#include "abstract_client.h" +#include "../abstract/abstract_client.h" #include diff --git a/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt new file mode 100644 index 000000000..0548700e4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/client/remote/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS remote_client.h) + +set(SOURCES remote_client.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_client_remote STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_client_remote libcockatrice_network_client_abstract libcockatrice_protocol) + +target_include_directories(libcockatrice_network_client_remote PUBLIC .) + +target_link_libraries( + libcockatrice_network_client_remote + PUBLIC ${COCKATRICE_QT_VERSION_NAME}::Network ${COCKATRICE_QT_VERSION_NAME}::WebSockets + libcockatrice_network_client_abstract libcockatrice_interfaces libcockatrice_utility libcockatrice_protocol +) diff --git a/cockatrice/src/server/remote/remote_client.cpp b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp similarity index 90% rename from cockatrice/src/server/remote/remote_client.cpp rename to libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp index 1d8579c14..3e3ec889d 100644 --- a/cockatrice/src/server/remote/remote_client.cpp +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.cpp @@ -1,18 +1,5 @@ #include "remote_client.h" -#include "../../main.h" -#include "../../settings/cache_settings.h" -#include "../pending_command.h" -#include "debug_pb_message.h" -#include "passwordhasher.h" -#include "pb/event_server_identification.pb.h" -#include "pb/response_activate.pb.h" -#include "pb/response_forgotpasswordrequest.pb.h" -#include "pb/response_login.pb.h" -#include "pb/response_password_salt.pb.h" -#include "pb/response_register.pb.h" -#include "pb/server_message.pb.h" -#include "pb/session_commands.pb.h" #include "version_string.h" #include @@ -23,17 +10,29 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include static const unsigned int protocolVersion = 14; -RemoteClient::RemoteClient(QObject *parent) - : AbstractClient(parent), timeRunning(0), lastDataReceived(0), messageInProgress(false), handshakeStarted(false), - usingWebSocket(false), messageLength(0), hashedPassword() +RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSettingsProvider) + : AbstractClient(parent), networkSettingsProvider(_networkSettingsProvider), timeRunning(0), lastDataReceived(0), + messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword() { clearNewClientFeatures(); - maxTimeout = SettingsCache::instance().getTimeOut(); - int keepalive = SettingsCache::instance().getKeepAlive(); + maxTimeout = networkSettingsProvider->getTimeOut(); + int keepalive = networkSettingsProvider->getKeepAlive(); timer = new QTimer(this); timer->setInterval(keepalive * 1000); connect(timer, &QTimer::timeout, this, &RemoteClient::ping); @@ -43,11 +42,7 @@ RemoteClient::RemoteClient(QObject *parent) connect(socket, &QTcpSocket::connected, this, &RemoteClient::slotConnected); connect(socket, &QTcpSocket::readyRead, this, &RemoteClient::readData); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) connect(socket, &QTcpSocket::errorOccurred, this, &RemoteClient::slotSocketError); -#else - connect(socket, qOverload(&QTcpSocket::error), this, &RemoteClient::slotSocketError); -#endif websocket = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, this); connect(websocket, &QWebSocket::binaryMessageReceived, this, &RemoteClient::websocketMessageReceived); @@ -308,8 +303,8 @@ void RemoteClient::loginResponse(const Response &response) emit ignoreListReceived(ignoreList); if (newMissingFeatureFound(possibleMissingFeatures) && resp.missing_features_size() > 0 && - SettingsCache::instance().getNotifyAboutUpdates()) { - SettingsCache::instance().setKnownMissingFeatures(possibleMissingFeatures); + networkSettingsProvider->getNotifyAboutUpdates()) { + networkSettingsProvider->setKnownMissingFeatures(possibleMissingFeatures); emit notifyUserAboutUpdate(); } @@ -395,14 +390,18 @@ void RemoteClient::readData() return; ServerMessage newServerMessage; - newServerMessage.ParseFromArray(inputBuffer.data(), messageLength); - - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); + bool ok = newServerMessage.ParseFromArray(inputBuffer.data(), messageLength); inputBuffer.remove(0, messageLength); messageInProgress = false; - processProtocolItem(newServerMessage); + if (ok) { + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); + + processProtocolItem(newServerMessage); + } else { + qCDebug(RemoteClientLog) << "parsing error!"; + } if (getStatus() == StatusDisconnecting) // use thread-safe getter doDisconnectFromServer(); @@ -413,11 +412,13 @@ void RemoteClient::websocketMessageReceived(const QByteArray &message) { lastDataReceived = timeRunning; ServerMessage newServerMessage; - newServerMessage.ParseFromArray(message.data(), message.length()); + if (newServerMessage.ParseFromArray(message.data(), message.length())) { + qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage); - - processProtocolItem(newServerMessage); + processProtocolItem(newServerMessage); + } else { + qCDebug(RemoteClientLog) << "parsing error!"; + } } void RemoteClient::sendCommandContainer(const CommandContainer &cont) @@ -431,19 +432,27 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont) qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont); QByteArray buf; + bool ok; if (usingWebSocket) { buf.resize(size); - cont.SerializeToArray(buf.data(), size); - websocket->sendBinaryMessage(buf); + ok = cont.SerializeToArray(buf.data(), size); + if (ok) { + websocket->sendBinaryMessage(buf); + } } else { buf.resize(size + 4); - cont.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char)size; - buf.data()[2] = (unsigned char)(size >> 8); - buf.data()[1] = (unsigned char)(size >> 16); - buf.data()[0] = (unsigned char)(size >> 24); + ok = cont.SerializeToArray(buf.data() + 4, size); + if (ok) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); - socket->write(buf); + socket->write(buf); + } + } + if (!ok) { + qCDebug(RemoteClientLog) << "transmit error!"; } } @@ -586,7 +595,7 @@ void RemoteClient::disconnectFromServer() QString RemoteClient::getSrvClientID(const QString &_hostname) { - QString srvClientID = SettingsCache::instance().getClientID(); + QString srvClientID = networkSettingsProvider->getClientID(); QHostInfo hostInfo = QHostInfo::fromName(_hostname); if (!hostInfo.error()) { QHostAddress hostAddress = hostInfo.addresses().first(); @@ -606,7 +615,7 @@ bool RemoteClient::newMissingFeatureFound(const QString &_serversMissingFeatures QStringList serversMissingFeaturesList = _serversMissingFeatures.split(","); for (const QString &feature : serversMissingFeaturesList) { if (!feature.isEmpty()) { - if (!SettingsCache::instance().getKnownMissingFeatures().contains(feature)) + if (!networkSettingsProvider->getKnownMissingFeatures().contains(feature)) return true; } } @@ -616,14 +625,14 @@ bool RemoteClient::newMissingFeatureFound(const QString &_serversMissingFeatures void RemoteClient::clearNewClientFeatures() { QString newKnownMissingFeatures; - QStringList existingKnownMissingFeatures = SettingsCache::instance().getKnownMissingFeatures().split(","); + QStringList existingKnownMissingFeatures = networkSettingsProvider->getKnownMissingFeatures().split(","); for (const QString &existingKnownFeature : existingKnownMissingFeatures) { if (!existingKnownFeature.isEmpty()) { if (!clientFeatures.contains(existingKnownFeature)) newKnownMissingFeatures.append("," + existingKnownFeature); } } - SettingsCache::instance().setKnownMissingFeatures(newKnownMissingFeatures); + networkSettingsProvider->setKnownMissingFeatures(newKnownMissingFeatures); } void RemoteClient::requestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName) diff --git a/cockatrice/src/server/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h similarity index 94% rename from cockatrice/src/server/remote/remote_client.h rename to libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index 0ab24c798..15e3e8ef5 100644 --- a/cockatrice/src/server/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -1,12 +1,18 @@ +/** + * @file remote_client.h + * @ingroup Client + * @brief TODO: Document this. + */ + #ifndef REMOTECLIENT_H #define REMOTECLIENT_H -#include "../abstract_client.h" -#include "pb/commands.pb.h" +#include "../abstract/abstract_client.h" #include -#include #include +#include +#include inline Q_LOGGING_CATEGORY(RemoteClientLog, "remote_client"); @@ -91,6 +97,7 @@ private slots: void submitForgotPasswordChallengeResponse(const Response &response); private: + INetworkSettingsProvider *networkSettingsProvider; int maxTimeout; int timeRunning, lastDataReceived; QByteArray inputBuffer; @@ -114,7 +121,7 @@ protected slots: void sendCommandContainer(const CommandContainer &cont) override; public: - explicit RemoteClient(QObject *parent = nullptr); + explicit RemoteClient(QObject *parent = nullptr, INetworkSettingsProvider *networkSettingsProvider = nullptr); ~RemoteClient() override; QString peerName() const { diff --git a/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt new file mode 100644 index 000000000..cbb717ad8 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/CMakeLists.txt @@ -0,0 +1,15 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_subdirectory(local) +add_subdirectory(remote) + +add_library(libcockatrice_network_server INTERFACE) + +target_include_directories(libcockatrice_network_server INTERFACE .) + +target_link_libraries( + libcockatrice_network_server INTERFACE ${COCKATRICE_QT_MODULES} libcockatrice_network_server_local + libcockatrice_network_server_remote +) diff --git a/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt new file mode 100644 index 000000000..80fb379a4 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/local/CMakeLists.txt @@ -0,0 +1,21 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS local_server.h local_server_interface.h) + +set(SOURCES local_server.cpp local_server_interface.cpp) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library(libcockatrice_network_server_local STATIC ${MOC_SOURCES} ${SOURCES}) + +add_dependencies(libcockatrice_network_server_local libcockatrice_protocol) + +target_include_directories(libcockatrice_network_server_local PUBLIC .) + +target_link_libraries(libcockatrice_network_server_local PUBLIC ${COCKATRICE_QT_MODULES} libcockatrice_protocol) diff --git a/cockatrice/src/server/local_server.cpp b/libcockatrice_network/libcockatrice/network/server/local/local_server.cpp similarity index 98% rename from cockatrice/src/server/local_server.cpp rename to libcockatrice_network/libcockatrice/network/server/local/local_server.cpp index c40247c72..8f9d82aa4 100644 --- a/cockatrice/src/server/local_server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server.cpp @@ -1,7 +1,8 @@ #include "local_server.h" #include "local_server_interface.h" -#include "server_room.h" + +#include <../remote/server_room.h> LocalServer::LocalServer(QObject *parent) : Server(parent) { diff --git a/cockatrice/src/server/local_server.h b/libcockatrice_network/libcockatrice/network/server/local/local_server.h similarity index 89% rename from cockatrice/src/server/local_server.h rename to libcockatrice_network/libcockatrice/network/server/local/local_server.h index 5f7d480dc..70586f6c1 100644 --- a/cockatrice/src/server/local_server.h +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server.h @@ -1,8 +1,14 @@ +/** + * @file local_server.h + * @ingroup Server + * @brief TODO: Document this. + */ + #ifndef LOCALSERVER_H #define LOCALSERVER_H -#include "server.h" -#include "server_database_interface.h" +#include <../remote/server.h> +#include <../remote/server_database_interface.h> class LocalServerInterface; diff --git a/cockatrice/src/server/local_server_interface.cpp b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.cpp similarity index 100% rename from cockatrice/src/server/local_server_interface.cpp rename to libcockatrice_network/libcockatrice/network/server/local/local_server_interface.cpp diff --git a/cockatrice/src/server/local_server_interface.h b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h similarity index 81% rename from cockatrice/src/server/local_server_interface.h rename to libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h index 8e997ae30..4410fd65c 100644 --- a/cockatrice/src/server/local_server_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/local/local_server_interface.h @@ -1,7 +1,13 @@ +/** + * @file local_server_interface.h + * @ingroup Server + * @brief TODO: Document this. + */ + #ifndef LOCALSERVERINTERFACE_H #define LOCALSERVERINTERFACE_H -#include "server_protocolhandler.h" +#include <../remote/server_protocolhandler.h> class LocalServer; @@ -19,7 +25,7 @@ public: QString getConnectionType() const override { return "local"; - }; + } void transmitProtocolItem(const ServerMessage &item) override; signals: void itemToClient(const ServerMessage &item); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt new file mode 100644 index 000000000..e883baa0d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/CMakeLists.txt @@ -0,0 +1,63 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + game/server_abstract_participant.h + game/server_abstract_player.h + game/server_arrow.h + game/server_arrowtarget.h + game/server_card.h + game/server_cardzone.h + game/server_counter.h + game/server_game.h + game/server_player.h + game/server_spectator.h + server.h + server_abstractuserinterface.h + server_database_interface.h + server_protocolhandler.h + server_remoteuserinterface.h + server_response_containers.h + server_room.h + serverinfo_user_container.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_network_server_remote STATIC + ${MOC_SOURCES} + game/server_abstract_participant.cpp + game/server_abstract_player.cpp + game/server_arrow.cpp + game/server_arrowtarget.cpp + game/server_card.cpp + game/server_cardzone.cpp + game/server_counter.cpp + game/server_game.cpp + game/server_player.cpp + game/server_spectator.cpp + server.cpp + server_abstractuserinterface.cpp + server_database_interface.cpp + server_protocolhandler.cpp + server_remoteuserinterface.cpp + server_response_containers.cpp + server_room.cpp + serverinfo_user_container.cpp +) + +add_dependencies(libcockatrice_network_server_remote libcockatrice_protocol) + +target_include_directories(libcockatrice_network_server_remote PUBLIC .) + +# Make cockatrice_server depend on cockatrice_protocol +target_link_libraries( + libcockatrice_network_server_remote PUBLIC libcockatrice_protocol libcockatrice_utility libcockatrice_rng + libcockatrice_deck_list ${COCKATRICE_QT_MODULES} +) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp new file mode 100644 index 000000000..493b8e966 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -0,0 +1,575 @@ +#include "server_abstract_participant.h" + +#include "../server.h" +#include "../server_abstractuserinterface.h" +#include "../server_database_interface.h" +#include "../server_room.h" +#include "server_card.h" +#include "server_game.h" +#include "server_player.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : ServerInfo_User_Container(_userInfo), game(_game), userInterface(_userInterface), pingTime(0), + playerId(_playerId), judge(_judge) +{ +} + +Server_AbstractParticipant::~Server_AbstractParticipant() = default; + +void Server_AbstractParticipant::removeFromGame() +{ + QMutexLocker locker(&playerMutex); + if (userInterface) { + userInterface->playerRemovedFromGame(game); + } +} + +bool Server_AbstractParticipant::updatePingTime() // returns true if ping time changed +{ + QMutexLocker locker(&playerMutex); + + int oldPingTime = pingTime; + if (userInterface) { + pingTime = userInterface->getLastCommandTime(); + } else { + pingTime = -1; + } + + return pingTime != oldPingTime; +} + +void Server_AbstractParticipant::getProperties(ServerInfo_PlayerProperties &result, bool withUserInfo) +{ + result.set_player_id(playerId); + if (withUserInfo) { + copyUserInfo(*(result.mutable_user_info()), true); + } + result.set_spectator(spectator); + result.set_judge(judge); + result.set_ping_seconds(pingTime); + getPlayerProperties(result); +} + +void Server_AbstractParticipant::getPlayerProperties(ServerInfo_PlayerProperties & /*result*/) +{ +} + +Response::ResponseCode Server_AbstractParticipant::cmdLeaveGame(const Command_LeaveGame & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + game->removeParticipant(this, Event_Leave::USER_LEFT); + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdKickFromGame(const Command_KickFromGame &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if ((game->getHostId() != playerId) && !(userInfo->user_level() & ServerInfo_User::IsModerator)) { + return Response::RespFunctionNotAllowed; + } + + if (!game->kickParticipant(cmd.player_id())) { + return Response::RespNameNotFound; + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDeckSelect(const Command_DeckSelect & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardPlan(const Command_SetSideboardPlan & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Command_SetSideboardLock & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdUnconcede(const Command_Unconcede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode +Server_AbstractParticipant::cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + auto *player = this->game->getPlayer(cmd.target_id()); + + ges.setForcedByJudge(playerId); + if (player == nullptr) { + return Response::RespContextError; + } + + for (int i = 0; i < cmd.game_command_size(); ++i) { + player->processGameCommand(cmd.game_command(i), rc, ges); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdReadyStart(const Command_ReadyStart & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode +Server_AbstractParticipant::cmdGameSay(const Command_GameSay &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (spectator) { + /* Spectators can only talk if: + * (a) the game creator allows it + * (b) the spectator is a moderator/administrator + * (c) the spectator is a judge + */ + bool isModOrJudge = (userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsJudge)); + if (!isModOrJudge && !game->getSpectatorsCanTalk()) { + return Response::RespFunctionNotAllowed; + } + } + + if (!userInterface->addSaidMessageSize(static_cast(cmd.message().size()))) { + return Response::RespChatFlood; + } + Event_GameSay event; + event.set_message(cmd.message()); + ges.enqueueGameEvent(event, playerId); + + game->getRoom()->getServer()->getDatabaseInterface()->logMessage( + userInfo->id(), QString::fromStdString(userInfo->name()), QString::fromStdString(userInfo->address()), + textFromStdString(cmd.message()), Server_DatabaseInterface::MessageTargetGame, game->getGameId(), + game->getDescription()); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdShuffle(const Command_Shuffle & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdMulligan(const Command_Mulligan & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdRollDie(const Command_RollDie & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) const +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDrawCards(const Command_DrawCards & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdMoveCard(const Command_MoveCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdFlipCard(const Command_FlipCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdAttachCard(const Command_AttachCard & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateToken(const Command_CreateToken & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateArrow(const Command_CreateArrow & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDeleteArrow(const Command_DeleteArrow & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCardAttr(const Command_SetCardAttr & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCardCounter(const Command_SetCardCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdIncCardCounter(const Command_IncCardCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdIncCounter(const Command_IncCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdCreateCounter(const Command_CreateCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetCounter(const Command_SetCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDelCounter(const Command_DelCounter & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdNextTurn(const Command_NextTurn & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + game->nextTurn(); + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdSetActivePhase(const Command_SetActivePhase &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + return Response::RespFunctionNotAllowed; + } + + game->setActivePhase(cmd.phase()); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractParticipant::cmdDumpZone(const Command_DumpZone & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdRevealCards(const Command_RevealCards & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdChangeZoneProperties(const Command_ChangeZoneProperties & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + return Response::RespFunctionNotAllowed; +} + +Response::ResponseCode Server_AbstractParticipant::cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!judge) { + if (spectator) { + return Response::RespFunctionNotAllowed; + } + + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + } + + bool reversedTurn = game->reverseTurnOrder(); + + Event_ReverseTurn event; + event.set_reversed(reversedTurn); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractParticipant::processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges) +{ + switch ((GameCommand::GameCommandType)getPbExtension(command)) { + case GameCommand::KICK_FROM_GAME: + return cmdKickFromGame(command.GetExtension(Command_KickFromGame::ext), rc, ges); + break; + case GameCommand::LEAVE_GAME: + return cmdLeaveGame(command.GetExtension(Command_LeaveGame::ext), rc, ges); + break; + case GameCommand::GAME_SAY: + return cmdGameSay(command.GetExtension(Command_GameSay::ext), rc, ges); + break; + case GameCommand::SHUFFLE: + return cmdShuffle(command.GetExtension(Command_Shuffle::ext), rc, ges); + break; + case GameCommand::MULLIGAN: + return cmdMulligan(command.GetExtension(Command_Mulligan::ext), rc, ges); + break; + case GameCommand::ROLL_DIE: + return cmdRollDie(command.GetExtension(Command_RollDie::ext), rc, ges); + break; + case GameCommand::DRAW_CARDS: + return cmdDrawCards(command.GetExtension(Command_DrawCards::ext), rc, ges); + break; + case GameCommand::UNDO_DRAW: + return cmdUndoDraw(command.GetExtension(Command_UndoDraw::ext), rc, ges); + break; + case GameCommand::FLIP_CARD: + return cmdFlipCard(command.GetExtension(Command_FlipCard::ext), rc, ges); + break; + case GameCommand::ATTACH_CARD: + return cmdAttachCard(command.GetExtension(Command_AttachCard::ext), rc, ges); + break; + case GameCommand::CREATE_TOKEN: + return cmdCreateToken(command.GetExtension(Command_CreateToken::ext), rc, ges); + break; + case GameCommand::CREATE_ARROW: + return cmdCreateArrow(command.GetExtension(Command_CreateArrow::ext), rc, ges); + break; + case GameCommand::DELETE_ARROW: + return cmdDeleteArrow(command.GetExtension(Command_DeleteArrow::ext), rc, ges); + break; + case GameCommand::SET_CARD_ATTR: + return cmdSetCardAttr(command.GetExtension(Command_SetCardAttr::ext), rc, ges); + break; + case GameCommand::SET_CARD_COUNTER: + return cmdSetCardCounter(command.GetExtension(Command_SetCardCounter::ext), rc, ges); + break; + case GameCommand::INC_CARD_COUNTER: + return cmdIncCardCounter(command.GetExtension(Command_IncCardCounter::ext), rc, ges); + break; + case GameCommand::READY_START: + return cmdReadyStart(command.GetExtension(Command_ReadyStart::ext), rc, ges); + break; + case GameCommand::CONCEDE: + return cmdConcede(command.GetExtension(Command_Concede::ext), rc, ges); + break; + case GameCommand::INC_COUNTER: + return cmdIncCounter(command.GetExtension(Command_IncCounter::ext), rc, ges); + break; + case GameCommand::CREATE_COUNTER: + return cmdCreateCounter(command.GetExtension(Command_CreateCounter::ext), rc, ges); + break; + case GameCommand::SET_COUNTER: + return cmdSetCounter(command.GetExtension(Command_SetCounter::ext), rc, ges); + break; + case GameCommand::DEL_COUNTER: + return cmdDelCounter(command.GetExtension(Command_DelCounter::ext), rc, ges); + break; + case GameCommand::NEXT_TURN: + return cmdNextTurn(command.GetExtension(Command_NextTurn::ext), rc, ges); + break; + case GameCommand::SET_ACTIVE_PHASE: + return cmdSetActivePhase(command.GetExtension(Command_SetActivePhase::ext), rc, ges); + break; + case GameCommand::DUMP_ZONE: + return cmdDumpZone(command.GetExtension(Command_DumpZone::ext), rc, ges); + break; + case GameCommand::REVEAL_CARDS: + return cmdRevealCards(command.GetExtension(Command_RevealCards::ext), rc, ges); + break; + case GameCommand::MOVE_CARD: + return cmdMoveCard(command.GetExtension(Command_MoveCard::ext), rc, ges); + break; + case GameCommand::SET_SIDEBOARD_PLAN: + return cmdSetSideboardPlan(command.GetExtension(Command_SetSideboardPlan::ext), rc, ges); + break; + case GameCommand::DECK_SELECT: + return cmdDeckSelect(command.GetExtension(Command_DeckSelect::ext), rc, ges); + break; + case GameCommand::SET_SIDEBOARD_LOCK: + return cmdSetSideboardLock(command.GetExtension(Command_SetSideboardLock::ext), rc, ges); + break; + case GameCommand::CHANGE_ZONE_PROPERTIES: + return cmdChangeZoneProperties(command.GetExtension(Command_ChangeZoneProperties::ext), rc, ges); + break; + case GameCommand::UNCONCEDE: + return cmdUnconcede(command.GetExtension(Command_Unconcede::ext), rc, ges); + break; + case GameCommand::JUDGE: + return cmdJudge(command.GetExtension(Command_Judge::ext), rc, ges); + break; + case GameCommand::REVERSE_TURN: + return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges); + break; + default: + return Response::RespInvalidCommand; + } +} + +void Server_AbstractParticipant::sendGameEvent(const GameEventContainer &cont) +{ + QMutexLocker locker(&playerMutex); + + if (userInterface) { + userInterface->sendProtocolItem(cont); + } +} + +void Server_AbstractParticipant::setUserInterface(Server_AbstractUserInterface *_userInterface) +{ + playerMutex.lock(); + userInterface = _userInterface; + playerMutex.unlock(); + + pingTime = _userInterface ? 0 : -1; + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_ping_seconds(pingTime); + + GameEventStorage ges; + ges.setGameEventContext(Context_ConnectionStateChanged()); + ges.enqueueGameEvent(event, playerId); + ges.sendToGame(game); +} + +void Server_AbstractParticipant::disconnectClient() +{ + bool isRegistered = userInfo->user_level() & ServerInfo_User::IsRegistered; + if (!isRegistered || spectator) { + game->removeParticipant(this, Event_Leave::USER_DISCONNECTED); + } else { + setUserInterface(nullptr); + } +} + +void Server_AbstractParticipant::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant * /*recipient*/, + bool /* omniscient */, + bool withUserInfo) +{ + getProperties(*info->mutable_properties(), withUserInfo); +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h new file mode 100644 index 000000000..a24fa5799 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.h @@ -0,0 +1,183 @@ +#ifndef ABSTRACT_PARTICIPANT_H +#define ABSTRACT_PARTICIPANT_H + +#include "../serverinfo_user_container.h" +#include "server_arrowtarget.h" + +#include +#include +#include + +class Server_Game; +class Server_AbstractUserInterface; +class ServerInfo_User; +class ServerInfo_Player; +class ServerInfo_PlayerProperties; +class GameEventContainer; +class GameEventStorage; +class ResponseContainer; +class GameCommand; + +class Command_KickFromGame; +class Command_LeaveGame; +class Command_GameSay; +class Command_Shuffle; +class Command_Mulligan; +class Command_RollDie; +class Command_DrawCards; +class Command_UndoDraw; +class Command_FlipCard; +class Command_AttachCard; +class Command_CreateToken; +class Command_CreateArrow; +class Command_DeleteArrow; +class Command_SetCardAttr; +class Command_SetCardCounter; +class Command_IncCardCounter; +class Command_ReadyStart; +class Command_Concede; +class Command_Unconcede; +class Command_Judge; +class Command_IncCounter; +class Command_CreateCounter; +class Command_SetCounter; +class Command_DelCounter; +class Command_NextTurn; +class Command_SetActivePhase; +class Command_DumpZone; +class Command_RevealCards; +class Command_ReverseTurn; +class Command_MoveCard; +class Command_SetSideboardPlan; +class Command_DeckSelect; +class Command_SetSideboardLock; +class Command_ChangeZoneProperties; + +class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container +{ + Q_OBJECT +protected: + Server_Game *game; + Server_AbstractUserInterface *userInterface; + int pingTime; + int playerId; + bool spectator; + bool judge; + virtual void getPlayerProperties(ServerInfo_PlayerProperties &result); + mutable QMutex playerMutex; + +public: + Server_AbstractParticipant(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_AbstractParticipant() override; + virtual void prepareDestroy() + { + removeFromGame(); + } + void removeFromGame(); + Server_AbstractUserInterface *getUserInterface() const + { + return userInterface; + } + void setUserInterface(Server_AbstractUserInterface *_userInterface); + void disconnectClient(); + + int getPlayerId() const + { + return playerId; + } + bool isSpectator() const + { + return spectator; + } + bool isJudge() const + { + return judge; + } + Server_Game *getGame() const + { + return game; + } + int getPingTime() const + { + return pingTime; + } + bool updatePingTime(); + void getProperties(ServerInfo_PlayerProperties &result, bool withUserInfo); + + virtual Response::ResponseCode + cmdLeaveGame(const Command_LeaveGame &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdKickFromGame(const Command_KickFromGame &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdJudge(const Command_Judge &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdMulligan(const Command_Mulligan &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdRollDie(const Command_RollDie &cmd, ResponseContainer &rc, GameEventStorage &ges) const; + virtual Response::ResponseCode + cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdUndoDraw(const Command_UndoDraw &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCardCounter(const Command_SetCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdIncCardCounter(const Command_IncCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges); + virtual Response::ResponseCode + cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges); + virtual Response::ResponseCode + cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, ResponseContainer &rc, GameEventStorage &ges); + + Response::ResponseCode processGameCommand(const GameCommand &command, ResponseContainer &rc, GameEventStorage &ges); + void sendGameEvent(const GameEventContainer &event); + + virtual void + getInfo(ServerInfo_Player *info, Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo); +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp new file mode 100644 index 000000000..7c0437bf0 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -0,0 +1,1652 @@ +#include "server_abstract_player.h" + +#include "server_arrow.h" +#include "server_card.h" +#include "server_cardzone.h" +#include "server_game.h" +#include "server_move_card_struct.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_AbstractPlayer::Server_AbstractPlayer(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface), conceded(false), deck(nullptr), + sideboardLocked(true), readyStart(false), nextCardId(0) +{ + spectator = false; +} + +Server_AbstractPlayer::~Server_AbstractPlayer() = default; + +void Server_AbstractPlayer::prepareDestroy() +{ + delete deck; + deck = nullptr; + + removeFromGame(); + clearZones(); + + deleteLater(); +} + +int Server_AbstractPlayer::newCardId() +{ + return nextCardId++; +} + +int Server_AbstractPlayer::newArrowId() const +{ + int id = 0; + for (Server_Arrow *a : arrows) { + if (a->getId() > id) { + id = a->getId(); + } + } + return id + 1; +} + +void Server_AbstractPlayer::setupZones() +{ + nextCardId = 0; +} + +void Server_AbstractPlayer::clearZones() +{ + for (Server_CardZone *zone : zones) { + delete zone; + } + zones.clear(); + + for (Server_Arrow *arrow : arrows) { + delete arrow; + } + arrows.clear(); +} + +void Server_AbstractPlayer::addZone(Server_CardZone *zone) +{ + zones.insert(zone->getName(), zone); +} + +void Server_AbstractPlayer::addArrow(Server_Arrow *arrow) +{ + arrows.insert(arrow->getId(), arrow); +} + +void Server_AbstractPlayer::updateArrowId(int id) +{ + auto *arrow = arrows.take(id); + arrows.insert(arrow->getId(), arrow); +} + +bool Server_AbstractPlayer::deleteArrow(int arrowId) +{ + Server_Arrow *arrow = arrows.value(arrowId, 0); + if (!arrow) { + return false; + } + arrows.remove(arrowId); + delete arrow; + return true; +} + +/** + * Creates the create token event. + * By default, will set event's name and color fields to empty if the token is face-down + */ +static Event_CreateToken +makeCreateTokenEvent(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, bool revealFacedownInfo = false) +{ + Event_CreateToken event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_face_down(card->getFaceDown()); + + if (!card->getFaceDown() || revealFacedownInfo) { + event.set_card_name(card->getName().toStdString()); + event.set_card_provider_id(card->getProviderId().toStdString()); + } + + event.set_color(card->getColor().toStdString()); + event.set_pt(card->getPT().toStdString()); + event.set_annotation(card->getAnnotation().toStdString()); + event.set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + event.set_x(xCoord); + event.set_y(yCoord); + return event; +} + +static Event_AttachCard makeAttachCardEvent(Server_Card *attachedCard, Server_Card *parentCard = nullptr) +{ + Event_AttachCard event; + event.set_start_zone(attachedCard->getZone()->getName().toStdString()); + event.set_card_id(attachedCard->getId()); + + if (parentCard) { + event.set_target_player_id(parentCard->getZone()->getPlayer()->getPlayerId()); + event.set_target_zone(parentCard->getZone()->getName().toStdString()); + event.set_target_card_id(parentCard->getId()); + } + + return event; +} + +/** + * Determines whether moving the card from startZone to targetZone should cause the card to be destroyed. + */ +static bool +shouldDestroyOnMove(const Server_Card *card, const Server_CardZone *startZone, const Server_CardZone *targetZone) +{ + if (!card->getDestroyOnZoneChange()) { + return false; + } + + if (startZone->getName() == targetZone->getName()) { + return false; + } + + // Allow tokens on the stack + if ((startZone->getName() == ZoneNames::TABLE || startZone->getName() == ZoneNames::STACK) && + (targetZone->getName() == ZoneNames::TABLE || targetZone->getName() == ZoneNames::STACK)) { + return false; + } + + return true; +} + +/** + * @brief Determines whether the moved card should be face-down + */ +static bool +shouldBeFaceDown(const MoveCardStruct &cardStruct, const Server_CardZone *startZone, const Server_CardZone *targetZone) +{ + if (!targetZone) { + return false; + } + + // being face-down only makes sense for public zones + if (targetZone->getType() != ServerInfo_Zone::PublicZone) { + return false; + } + + // face-down property in proto takes precedence + if (cardStruct.cardToMove->has_face_down()) { + return cardStruct.cardToMove->face_down(); + } + + // Default to keep face-down the same if zone didn't change. + // Compare using zone names because face-down is maintained when changing controllers. + if (startZone && startZone->getName() == targetZone->getName()) { + return cardStruct.card->getFaceDown(); + } + + return false; +} + +/** + * @brief Determines whether a set of moved cards is from the bottom of the deck + */ +static bool shouldBeFromTheBottom(const Server_CardZone *startZone, const std::set &cardsToMove) +{ + if (!startZone) { + return false; + } + + if (startZone->getName() != ZoneNames::DECK) { + return false; + } + + int movedCount = static_cast(cardsToMove.size()); + int tailStart = startZone->getCards().size() - movedCount; + if (tailStart <= 0) { // if the entire deck is moved it should not be considered from the bottom + return false; + } + + // check if the move is a contiguous block at the end of the deck, fail fast when not + int expectedPosition = tailStart; + for (const auto &card : cardsToMove) { + if (card.position != expectedPosition) { + return false; + } + ++expectedPosition; + } + + return true; +} + +Response::ResponseCode Server_AbstractPlayer::moveCard(GameEventStorage &ges, + Server_CardZone *startzone, + const QList &_cards, + Server_CardZone *targetzone, + int xCoord, + int yCoord, + bool fixFreeSpaces, + bool undoingDraw, + bool isReversed) +{ + // Disallow controller change to other zones than the table. + if (((targetzone->getType() != ServerInfo_Zone::PublicZone) || !targetzone->hasCoords()) && + (startzone->getPlayer() != targetzone->getPlayer()) && !judge) { + return Response::RespContextError; + } + + if (!targetzone->hasCoords()) { + yCoord = 0; + if (xCoord <= -1) { + xCoord = targetzone->getCards().size(); + } + } + + std::set cardsToMove; + QSet cardIdsToMove; + for (auto _card : _cards) { + // The same card being moved twice would lead to undefined behaviour. + if (cardIdsToMove.contains(_card->card_id())) { + continue; + } + cardIdsToMove.insert(_card->card_id()); + + // Consistency checks. In case the command contains illegal moves, try to resolve the legal ones still. + int position; + Server_Card *card = startzone->getCard(_card->card_id(), &position); + if (!card) { + return Response::RespNameNotFound; + } + + // do not allow attached cards to move around on the table + if (card->getParentCard() && targetzone->getName() == ZoneNames::TABLE) { + continue; + } + + // do not allow cards with attachments to stack with other cards + if (!card->getAttachedCards().isEmpty() && !targetzone->isColumnEmpty(xCoord, yCoord)) { + continue; + } + + cardsToMove.insert(MoveCardStruct{card, position, _card}); + } + // In case all moves were filtered out, abort. + if (cardsToMove.empty()) { + return Response::RespContextError; + } + + int xIndex = -1; + bool revealTopStart = false; + bool revealTopTarget = false; + + bool isFromBottom = shouldBeFromTheBottom(startzone, cardsToMove); + + if (isFromBottom) { + std::ranges::reverse_view reversedCardsToMove{cardsToMove}; + for (auto card : reversedCardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); + } + } else { + for (auto card : cardsToMove) { + processMoveCard(ges, startzone, targetzone, card, xCoord, yCoord, xIndex, revealTopStart, revealTopTarget, + isReversed, undoingDraw); + } + } + + if (revealTopStart) { + revealTopCardIfNeeded(startzone, ges); + } + if (targetzone != startzone && revealTopTarget) { + revealTopCardIfNeeded(targetzone, ges); + } + if (undoingDraw) { + ges.setGameEventContext(Context_UndoDraw()); + } else { + ges.setGameEventContext(Context_MoveCard()); + } + + if (startzone->hasCoords() && fixFreeSpaces) { + startzone->fixFreeSpaces(ges); + } + + return Response::RespOk; +} + +void Server_AbstractPlayer::processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw) +{ + Server_Card *card = cardStruct.card; + int originalPosition = cardStruct.position; + + bool sourceBeingLookedAt; + int position = startzone->removeCard(card, sourceBeingLookedAt); + + // Attachment relationships can be retained when moving a card onto the opponent's table + if (startzone->getName() != targetzone->getName()) { + // Delete all attachment relationships + if (card->getParentCard()) { + card->setParentCard(nullptr); + } + + // Make a copy of the list because the original one gets modified during the loop + QList attachedCards = card->getAttachedCards(); + for (auto &attachedCard : attachedCards) { + attachedCard->getZone()->getPlayer()->unattachCard(ges, attachedCard); + } + } + + if (startzone != targetzone) { + // Delete all arrows from and to the card + for (auto *player : game->getPlayers().values()) { + QList arrowsToDelete; + for (Server_Arrow *arrow : player->getArrows()) { + if ((arrow->getStartCard() == card) || (arrow->getTargetItem() == card)) + arrowsToDelete.append(arrow->getId()); + } + for (int j : arrowsToDelete) { + player->deleteArrow(j); + } + } + } + + if (shouldDestroyOnMove(card, startzone, targetzone)) { + Event_DestroyCard event; + event.set_zone_name(startzone->getName().toStdString()); + event.set_card_id(static_cast(card->getId())); + ges.enqueueGameEvent(event, playerId); + + if (Server_Card *stashedCard = card->takeStashedCard()) { + stashedCard->setId(newCardId()); + ges.enqueueGameEvent(makeCreateTokenEvent(startzone, stashedCard, card->getX(), card->getY()), playerId); + card->deleteLater(); + card = stashedCard; + } else { + card->deleteLater(); + card = nullptr; + } + } + + if (card) { + ++xIndex; + int newX = isReversed ? targetzone->getCards().size() - xCoord + xIndex : xCoord + xIndex; + + bool faceDown = shouldBeFaceDown(cardStruct, startzone, targetzone); + + if (targetzone->hasCoords()) { + newX = targetzone->getFreeGridColumn(newX, yCoord, card->getName(), faceDown); + } else { + card->resetState(targetzone->getName() == ZoneNames::STACK); + } + + targetzone->insertCard(card, newX, yCoord); + int targetLookedCards = targetzone->getCardsBeingLookedAt(); + bool sourceKnownToPlayer = isReversed || (sourceBeingLookedAt && !card->getFaceDown()); + if (targetzone->getType() == ServerInfo_Zone::HiddenZone && targetLookedCards >= newX) { + if (sourceKnownToPlayer) { + targetLookedCards += 1; + } else { + targetLookedCards = newX; + } + targetzone->setCardsBeingLookedAt(targetLookedCards); + } + + bool targetHiddenToOthers = faceDown || (targetzone->getType() != ServerInfo_Zone::PublicZone); + bool sourceHiddenToOthers = card->getFaceDown() || (startzone->getType() != ServerInfo_Zone::PublicZone); + + int oldCardId = card->getId(); + if ((faceDown && (startzone != targetzone)) || (targetzone->getPlayer() != startzone->getPlayer())) { + card->setId(targetzone->getPlayer()->newCardId()); + } + card->setFaceDown(faceDown); + + Event_MoveCard eventOthers; + eventOthers.set_start_player_id(startzone->getPlayer()->getPlayerId()); + eventOthers.set_start_zone(startzone->getName().toStdString()); + eventOthers.set_target_player_id(targetzone->getPlayer()->getPlayerId()); + if (startzone != targetzone) { + eventOthers.set_target_zone(targetzone->getName().toStdString()); + } + eventOthers.set_y(yCoord); + eventOthers.set_face_down(faceDown); + + Event_MoveCard eventPrivate(eventOthers); + if (sourceBeingLookedAt || targetzone->getType() != ServerInfo_Zone::HiddenZone || + startzone->getType() != ServerInfo_Zone::HiddenZone) { + eventPrivate.set_card_id(oldCardId); + eventPrivate.set_new_card_id(card->getId()); + } else { + eventPrivate.set_card_id(-1); + eventPrivate.set_new_card_id(-1); + } + if (sourceKnownToPlayer || !(faceDown || targetzone->getType() == ServerInfo_Zone::HiddenZone)) { + QString privateCardName = card->getName(); + eventPrivate.set_card_name(privateCardName.toStdString()); + eventPrivate.set_new_card_provider_id(card->getProviderId().toStdString()); + } + if (startzone->getType() == ServerInfo_Zone::HiddenZone) { + eventPrivate.set_position(position); + } else { + eventPrivate.set_position(-1); + } + + eventPrivate.set_x(newX); + + if ( + // cards from public zones have their id known, their previous position is already known, the event does + // not accomodate for previous locations in zones with coordinates (which are always public) + (startzone->getType() != ServerInfo_Zone::PublicZone) && + // other players are not allowed to be able to track which card is which in private zones like the hand + (startzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_position(position); + } + if ( + // other players are not allowed to be able to track which card is which in private zones like the hand + (targetzone->getType() != ServerInfo_Zone::PrivateZone)) { + eventOthers.set_x(newX); + } + + if ((startzone->getType() == ServerInfo_Zone::PublicZone) || + (targetzone->getType() == ServerInfo_Zone::PublicZone)) { + eventOthers.set_card_id(oldCardId); + if (!(sourceHiddenToOthers && targetHiddenToOthers)) { + QString publicCardName = card->getName(); + eventOthers.set_card_name(publicCardName.toStdString()); + eventOthers.set_new_card_provider_id(card->getProviderId().toStdString()); + } + eventOthers.set_new_card_id(card->getId()); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + if (originalPosition == 0) { + revealTopStart = true; + } + if (newX == 0) { + revealTopTarget = true; + } + + // handle side effects for this card + onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + } +} + +void Server_AbstractPlayer::onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool /*undoingDraw*/) +{ + Server_Card *card = cardStruct.card; + const CardToMove *thisCardProperties = cardStruct.cardToMove; + + // set card to be tapped + if (thisCardProperties->tapped()) { + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), AttrTapped, + "1"); + } + + // set card pt + QString ptString = QString::fromStdString(thisCardProperties->pt()); + if (!ptString.isEmpty()) { + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), AttrPT, + ptString); + } + + // If card is transferring to a different player, leave an annotation of who actually "owns" the card + const auto &priorAnnotation = card->getAnnotation(); + if (startzone->getPlayer() != targetzone->getPlayer() && !priorAnnotation.contains("Owner:")) { + const auto &ownerAnnotation = "Owner: " + QString::fromStdString(startzone->getPlayer()->getUserInfo()->name()); + const auto &newAnnotation = + priorAnnotation.isEmpty() ? ownerAnnotation : ownerAnnotation + "\n\n" + priorAnnotation; + setCardAttrHelper(ges, targetzone->getPlayer()->getPlayerId(), targetzone->getName(), card->getId(), + AttrAnnotation, newAnnotation, card); + } +} + +void Server_AbstractPlayer::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.add_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.add_card_id(0); + zone->getCards().first()->getInfo(revealEvent.add_cards()); + ges.enqueueGameEvent(revealEvent, playerId, GameEventStorageItem::SendToPrivate, playerId); + } +} + +void Server_AbstractPlayer::unattachCard(GameEventStorage &ges, Server_Card *card) +{ + Server_CardZone *zone = card->getZone(); + Server_Card *parentCard = card->getParentCard(); + card->setParentCard(nullptr); + + ges.enqueueGameEvent(makeAttachCardEvent(card), playerId); + + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(card->getId()); + moveCard(ges, zone, QList() << cardToMove, zone, -1, card->getY(), card->getFaceDown()); + delete cardToMove; + + if (parentCard->getZone()) { + parentCard->getZone()->updateCardCoordinates(parentCard, parentCard->getX(), parentCard->getY()); + } +} + +Response::ResponseCode Server_AbstractPlayer::setCardAttrHelper(GameEventStorage &ges, + int targetPlayerId, + const QString &zoneName, + int cardId, + CardAttribute attribute, + const QString &attrValue, + Server_Card *unzonedCard) +{ + Server_CardZone *zone = getZones().value(zoneName); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + QString result; + if (cardId == -1) { + QListIterator CardIterator(zone->getCards()); + while (CardIterator.hasNext()) { + result = CardIterator.next()->setAttribute(attribute, attrValue, true); + if (result.isNull()) { + return Response::RespInvalidCommand; + } + } + } else { + Server_Card *card = unzonedCard == nullptr ? zone->getCard(cardId) : unzonedCard; + if (!card) { + return Response::RespNameNotFound; + } + result = card->setAttribute(attribute, attrValue, false); + if (result.isNull()) { + return Response::RespInvalidCommand; + } + } + + Event_SetCardAttr event; + event.set_zone_name(zone->getName().toStdString()); + if (cardId != -1) { + event.set_card_id(cardId); + } + event.set_attribute(attribute); + event.set_attr_value(result.toStdString()); + ges.enqueueGameEvent(event, targetPlayerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + setConceded(true); + game->removeArrowsRelatedToPlayer(ges, this); + game->unattachCards(ges, this); + game->returnCardsFromPlayer(ges, this); + + clearZones(); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_conceded(true); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_Concede()); + + game->stopGameIfFinished(); + if (game->getGameStarted() && (game->getActivePlayer() == playerId)) { + game->nextTurn(); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdUnconcede(const Command_Unconcede & /*cmd*/, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (!conceded) { + return Response::RespContextError; + } + + setConceded(false); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_conceded(false); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_Unconcede()); + + setupZones(); + + game->sendGameStateToPlayers(); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!deck || game->getGameStarted()) { + return Response::RespContextError; + } + + if (readyStart == cmd.ready() && !cmd.force_start()) { + return Response::RespContextError; + } + + setReadyStart(cmd.ready()); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_ready_start(cmd.ready()); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_ReadyStart()); + + if (cmd.force_start()) { + if (game->getHostId() != playerId) { + return Response::RespFunctionNotAllowed; + } + game->startGameIfReady(true); + } else if (cmd.ready()) { + game->startGameIfReady(false); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdRollDie(const Command_RollDie &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) const +{ + if (conceded) { + return Response::RespContextError; + } + + const auto validatedSides = static_cast(std::min(std::max(cmd.sides(), MINIMUM_DIE_SIDES), MAXIMUM_DIE_SIDES)); + const auto validatedDiceToRoll = + static_cast(std::min(std::max(cmd.count(), MINIMUM_DICE_TO_ROLL), MAXIMUM_DICE_TO_ROLL)); + + Event_RollDie event; + event.set_sides(validatedSides); + for (auto i = 0; i < validatedDiceToRoll; ++i) { + const auto roll = rng->rand(1, validatedSides); + if (i == 0) { + // Backwards compatibility + event.set_value(roll); + } + event.add_values(roll); + } + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_AbstractPlayer *startPlayer = game->getPlayer(cmd.has_start_player_id() ? cmd.start_player_id() : playerId); + if (!startPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *startZone = startPlayer->getZones().value(nameFromStdString(cmd.start_zone())); + if (!startZone) { + return Response::RespNameNotFound; + } + + if ((startPlayer != this) && (!startZone->getPlayersWithWritePermission().contains(playerId)) && !judge) { + return Response::RespContextError; + } + + Server_AbstractPlayer *targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!targetPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *targetZone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + if (!targetZone) { + return Response::RespNameNotFound; + } + + if ((startPlayer != this) && (targetPlayer != this) && !judge) { + return Response::RespContextError; + } + + QList cardsToMove; + for (int i = 0; i < cmd.cards_to_move().card_size(); ++i) { + cardsToMove.append(&cmd.cards_to_move().card(i)); + } + + return moveCard(ges, startZone, cardsToMove, targetZone, cmd.x(), cmd.y(), true, false, cmd.is_reversed()); +} + +Response::ResponseCode +Server_AbstractPlayer::cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + const bool faceDown = cmd.face_down(); + if (faceDown == card->getFaceDown()) { + return Response::RespContextError; + } + + card->setFaceDown(faceDown); + + Event_FlipCard event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + if (!faceDown) { + event.set_card_name(card->getName().toStdString()); + event.set_card_provider_id(card->getProviderId().toStdString()); + } + event.set_face_down(faceDown); + ges.enqueueGameEvent(event, playerId); + + QString ptString = nameFromStdString(cmd.pt()); + if (!ptString.isEmpty() && !faceDown) { + setCardAttrHelper(ges, playerId, zone->getName(), card->getId(), AttrPT, ptString); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *startzone = zones.value(nameFromStdString(cmd.start_zone())); + if (!startzone) { + return Response::RespNameNotFound; + } + + Server_Card *card = startzone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + Server_AbstractPlayer *targetPlayer = nullptr; + Server_CardZone *targetzone = nullptr; + Server_Card *targetCard = nullptr; + + if (cmd.has_target_player_id()) { + targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!targetPlayer) { + return Response::RespNameNotFound; + } + } else if (!card->getParentCard()) { + return Response::RespContextError; + } + if (targetPlayer) { + targetzone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + } + if (targetzone) { + // This is currently enough to make sure cards don't get attached to a card that is not on the table. + // Possibly a flag will have to be introduced for this sometime. + if (!targetzone->hasCoords()) { + return Response::RespContextError; + } + if (cmd.has_target_card_id()) { + targetCard = targetzone->getCard(cmd.target_card_id()); + } + if (targetCard) { + if (targetCard->getParentCard()) { + return Response::RespContextError; + } + } else { + return Response::RespNameNotFound; + } + } + + // prevent attaching from non-table zones + // (attaching from non-table zones is handled client-side by moving the card to table zone first) + if (!startzone->hasCoords()) { + return Response::RespContextError; + } + + for (auto *player : game->getPlayers()) { + QList _arrows = player->getArrows().values(); + QList toDelete; + for (auto a : _arrows) { + auto *tCard = qobject_cast(a->getTargetItem()); + if ((tCard == card) || (a->getStartCard() == card)) { + toDelete.append(a); + } + } + for (auto &i : toDelete) { + Event_DeleteArrow event; + event.set_arrow_id(i->getId()); + ges.enqueueGameEvent(event, player->getPlayerId()); + player->deleteArrow(i->getId()); + } + } + + if (targetCard) { + // Unattach all cards attached to the card being attached. + // Make a copy of the list because its contents change during the loop otherwise. + QList attachedList = card->getAttachedCards(); + for (const auto &i : attachedList) { + i->getZone()->getPlayer()->unattachCard(ges, i); + } + + card->setParentCard(targetCard); + const int oldX = card->getX(); + card->setCoords(-1, card->getY()); + startzone->updateCardCoordinates(card, oldX, card->getY()); + + if (targetzone->isColumnStacked(targetCard->getX(), targetCard->getY())) { + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(targetCard->getId()); + targetPlayer->moveCard(ges, targetzone, QList() << cardToMove, targetzone, + targetzone->getFreeGridColumn(-2, targetCard->getY(), targetCard->getName(), false), + targetCard->getY(), targetCard->getFaceDown()); + delete cardToMove; + } + + ges.enqueueGameEvent(makeAttachCardEvent(card, targetCard), playerId); + + startzone->fixFreeSpaces(ges); + } else { + unattachCard(ges, card); + } + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + + int xCoord = cmd.x(); + int yCoord = cmd.y(); + + Server_Card *targetCard = nullptr; + if (cmd.has_target_card_id()) { + Server_CardZone *targetZone = zones.value(nameFromStdString(cmd.target_zone())); + if (targetZone) { + targetCard = targetZone->getCard(cmd.target_card_id()); + if (targetCard && cmd.target_mode() == Command_CreateToken::TRANSFORM_INTO) { + if (targetCard->getParentCard()) { + ges.enqueueGameEvent(makeAttachCardEvent(targetCard), playerId); + } + + for (Server_Card *attachedCard : targetCard->getAttachedCards()) { + ges.enqueueGameEvent(makeAttachCardEvent(attachedCard), + attachedCard->getZone()->getPlayer()->getPlayerId()); + } + + if (zone->hasCoords() && zone == targetZone) { + xCoord = targetCard->getX(); + yCoord = targetCard->getY(); + } + + targetZone->removeCard(targetCard); + + Event_DestroyCard event; + event.set_zone_name(targetZone->getName().toStdString()); + event.set_card_id(static_cast<::google::protobuf::uint32>(cmd.target_card_id())); + ges.enqueueGameEvent(event, playerId); + } + } + } + + const QString cardName = nameFromStdString(cmd.card_name()); + const QString cardProviderId = nameFromStdString(cmd.card_provider_id()); + if (zone->hasCoords()) { + bool dontStackSameName = cmd.face_down(); + xCoord = zone->getFreeGridColumn(xCoord, yCoord, cardName, dontStackSameName); + } + if (xCoord < 0) { + xCoord = 0; + } + if (yCoord < 0) { + yCoord = 0; + } + + auto *card = new Server_Card({cardName, cardProviderId}, newCardId(), xCoord, yCoord); + card->moveToThread(thread()); + // Client should already prevent face-down tokens from having attributes; this just an extra server-side check + if (!cmd.face_down()) { + card->setColor(nameFromStdString(cmd.color())); + card->setPT(nameFromStdString(cmd.pt())); + } + card->setAnnotation(nameFromStdString(cmd.annotation())); + card->setDestroyOnZoneChange(cmd.destroy_on_zone_change()); + card->setFaceDown(cmd.face_down()); + + zone->insertCard(card, xCoord, yCoord); + sendCreateTokenEvents(zone, card, xCoord, yCoord, ges); + + // check if the token is a replacement for an existing card + if (!targetCard) { + return Response::RespOk; + } + + switch (cmd.target_mode()) { + case Command_CreateToken::ATTACH_TO: { + Command_AttachCard cmd2; + cmd2.set_start_zone(cmd.target_zone()); + cmd2.set_card_id(cmd.target_card_id()); + + cmd2.set_target_player_id(zone->getPlayer()->getPlayerId()); + cmd2.set_target_zone(cmd.zone()); + cmd2.set_target_card_id(card->getId()); + + return cmdAttachCard(cmd2, rc, ges); + } + + case Command_CreateToken::TRANSFORM_INTO: { + // Copy attributes that are not present in the CreateToken event + Event_SetCardAttr event; + event.set_zone_name(card->getZone()->getName().toStdString()); + event.set_card_id(card->getId()); + + if (card->getTapped() != targetCard->getTapped()) { + card->setAttribute(AttrTapped, QVariant(targetCard->getTapped()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getAttacking() != targetCard->getAttacking()) { + card->setAttribute(AttrAttacking, QVariant(targetCard->getAttacking()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getFaceDown() != targetCard->getFaceDown()) { + card->setAttribute(AttrFaceDown, QVariant(targetCard->getFaceDown()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + if (card->getDoesntUntap() != targetCard->getDoesntUntap()) { + card->setAttribute(AttrDoesntUntap, QVariant(targetCard->getDoesntUntap()).toString(), &event); + ges.enqueueGameEvent(event, playerId); + } + + // Copy counters + QMapIterator i(targetCard->getCounters()); + while (i.hasNext()) { + i.next(); + + Event_SetCardCounter _event; + _event.set_zone_name(card->getZone()->getName().toStdString()); + _event.set_card_id(card->getId()); + + card->setCounter(i.key(), i.value(), &_event); + ges.enqueueGameEvent(_event, playerId); + } + + // Copy parent card + if (Server_Card *parentCard = targetCard->getParentCard()) { + targetCard->setParentCard(nullptr); + card->setParentCard(parentCard); + + ges.enqueueGameEvent(makeAttachCardEvent(card, parentCard), playerId); + } + + // Copy attachments + while (!targetCard->getAttachedCards().isEmpty()) { + Server_Card *attachedCard = targetCard->getAttachedCards().last(); + attachedCard->setParentCard(card); + + ges.enqueueGameEvent(makeAttachCardEvent(attachedCard, card), + attachedCard->getZone()->getPlayer()->getPlayerId()); + } + + // Copy Arrows + for (auto *player : game->getPlayers().values()) { + QList changedArrowIds; + for (Server_Arrow *arrow : player->getArrows()) { + bool sendGameEvent = false; + const auto *startCard = arrow->getStartCard(); + if (startCard == targetCard) { + sendGameEvent = true; + arrow->setStartCard(card); + startCard = card; + } + const auto *targetItem = arrow->getTargetItem(); + if (targetItem == targetCard) { + sendGameEvent = true; + arrow->setTargetItem(card); + targetItem = card; + } + if (sendGameEvent) { + Event_CreateArrow _event; + ServerInfo_Arrow *arrowInfo = _event.mutable_arrow_info(); + changedArrowIds.append(arrow->getId()); + int id = player->newArrowId(); + arrow->setId(id); + arrowInfo->set_id(id); + arrowInfo->set_start_player_id(player->getPlayerId()); + arrowInfo->set_start_zone(startCard->getZone()->getName().toStdString()); + arrowInfo->set_start_card_id(startCard->getId()); + const auto *arrowTargetPlayer = qobject_cast(targetItem); + if (arrowTargetPlayer != nullptr) { + arrowInfo->set_target_player_id(arrowTargetPlayer->getPlayerId()); + } else { + const auto *arrowTargetCard = qobject_cast(targetItem); + arrowInfo->set_target_player_id(arrowTargetCard->getZone()->getPlayer()->getPlayerId()); + arrowInfo->set_target_zone(arrowTargetCard->getZone()->getName().toStdString()); + arrowInfo->set_target_card_id(arrowTargetCard->getId()); + } + arrowInfo->mutable_arrow_color()->CopyFrom(arrow->getColor()); + ges.enqueueGameEvent(_event, player->getPlayerId()); + } + } + for (int id : changedArrowIds) { + player->updateArrowId(id); + } + } + + targetCard->resetState(); + card->setStashedCard(targetCard); + break; + } + } + + return Response::RespOk; +} + +/** + * Creates and sends the events required to properly communicate the given token creation. + * Primarily written to handle creating face-down tokens. + */ +void Server_AbstractPlayer::sendCreateTokenEvents(Server_CardZone *zone, + Server_Card *card, + int xCoord, + int yCoord, + GameEventStorage &ges) +{ + // Token is not face-down; things are easy + if (!card->getFaceDown()) { + ges.enqueueGameEvent(makeCreateTokenEvent(zone, card, xCoord, yCoord), playerId); + return; + } + + // Token is face-down. We have to send different info to each player + auto eventOthers = makeCreateTokenEvent(zone, card, xCoord, yCoord, false); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + auto eventPrivate = makeCreateTokenEvent(zone, card, xCoord, yCoord, true); + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + + // Event_CreateToken didn't use to have face_down field; send attribute event afterward for backwards compatibility + Event_SetCardAttr event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_attribute(AttrFaceDown); + event.set_attr_value("1"); + ges.enqueueGameEvent(event, playerId); +} + +Response::ResponseCode +Server_AbstractPlayer::cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_AbstractPlayer *startPlayer = game->getPlayer(cmd.start_player_id()); + Server_AbstractPlayer *targetPlayer = game->getPlayer(cmd.target_player_id()); + if (!startPlayer || !targetPlayer) { + return Response::RespNameNotFound; + } + QString startZoneName = nameFromStdString(cmd.start_zone()); + Server_CardZone *startZone = startPlayer->getZones().value(startZoneName); + bool playerTarget = !cmd.has_target_zone(); + Server_CardZone *targetZone = nullptr; + if (!playerTarget) { + targetZone = targetPlayer->getZones().value(nameFromStdString(cmd.target_zone())); + } + if (!startZone || (!targetZone && !playerTarget)) { + return Response::RespNameNotFound; + } + if (startZone->getType() != ServerInfo_Zone::PublicZone) { + return Response::RespContextError; + } + Server_Card *startCard = startZone->getCard(cmd.start_card_id()); + if (!startCard) { + return Response::RespNameNotFound; + } + Server_Card *targetCard = nullptr; + if (!playerTarget) { + if (targetZone->getType() != ServerInfo_Zone::PublicZone) { + return Response::RespContextError; + } + targetCard = targetZone->getCard(cmd.target_card_id()); + } + + Server_ArrowTarget *targetItem; + if (playerTarget) { + targetItem = targetPlayer; + } else { + targetItem = targetCard; + } + if (!targetItem) { + return Response::RespNameNotFound; + } + + for (Server_Arrow *temp : arrows) { + if ((temp->getStartCard() == startCard) && (temp->getTargetItem() == targetItem)) { + return Response::RespContextError; + } + } + + int currentPhase = game->getActivePhase(); + int deletionPhase = cmd.has_delete_in_phase() ? cmd.delete_in_phase() : currentPhase; + auto arrow = new Server_Arrow(newArrowId(), startCard, targetItem, cmd.arrow_color(), currentPhase, deletionPhase); + addArrow(arrow); + + Event_CreateArrow event; + ServerInfo_Arrow *arrowInfo = event.mutable_arrow_info(); + arrowInfo->set_id(arrow->getId()); + arrowInfo->set_start_player_id(startPlayer->getPlayerId()); + arrowInfo->set_start_zone(startZoneName.toStdString()); + arrowInfo->set_start_card_id(startCard->getId()); + arrowInfo->set_target_player_id(targetPlayer->getPlayerId()); + if (!playerTarget) { + arrowInfo->set_target_zone(cmd.target_zone()); + arrowInfo->set_target_card_id(cmd.target_card_id()); + } + arrowInfo->mutable_arrow_color()->CopyFrom(cmd.arrow_color()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (!deleteArrow(cmd.arrow_id())) { + return Response::RespNameNotFound; + } + + Event_DeleteArrow event; + event.set_arrow_id(cmd.arrow_id()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + return setCardAttrHelper(ges, playerId, nameFromStdString(cmd.zone()), cmd.card_id(), cmd.attribute(), + nameFromStdString(cmd.attr_value())); +} + +Response::ResponseCode Server_AbstractPlayer::cmdSetCardCounter(const Command_SetCardCounter &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + Event_SetCardCounter event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + card->setCounter(cmd.counter_id(), cmd.counter_value(), &event); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdIncCardCounter(const Command_IncCardCounter &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!zone->hasCoords()) { + return Response::RespContextError; + } + + Server_Card *card = zone->getCard(cmd.card_id()); + if (!card) { + return Response::RespNameNotFound; + } + + int newValue = card->getCounter(cmd.counter_id()) + cmd.counter_delta(); + card->setCounter(cmd.counter_id(), newValue); + + Event_SetCardCounter event; + event.set_zone_name(zone->getName().toStdString()); + event.set_card_id(card->getId()); + event.set_counter_id(cmd.counter_id()); + event.set_counter_value(newValue); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + Server_AbstractPlayer *otherPlayer = game->getPlayer(cmd.player_id()); + if (!otherPlayer) { + return Response::RespNameNotFound; + } + Server_CardZone *zone = otherPlayer->getZones().value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + if (!((zone->getType() == ServerInfo_Zone::PublicZone) || (this == otherPlayer))) { + return Response::RespContextError; + } + + int numberCards = cmd.number_cards(); + const QList &cards = zone->getCards(); + + auto *re = new Response_DumpZone; + ServerInfo_Zone *zoneInfo = re->mutable_zone_info(); + zoneInfo->set_name(zone->getName().toStdString()); + zoneInfo->set_type(zone->getType()); + zoneInfo->set_with_coords(zone->hasCoords()); + zoneInfo->set_card_count(numberCards < cards.size() ? cards.size() : numberCards); + + for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) { + const auto &findId = cmd.is_reversed() ? cards.size() - numberCards + i : i; + Server_Card *card = cards[findId]; + QString displayedName = card->getFaceDown() ? QString() : card->getName(); + ServerInfo_Card *cardInfo = zoneInfo->add_card_list(); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + cardInfo->set_name(displayedName.toStdString()); + if (zone->getType() == ServerInfo_Zone::HiddenZone) { + cardInfo->set_id(findId); + } else { + cardInfo->set_id(card->getId()); + cardInfo->set_x(card->getX()); + cardInfo->set_y(card->getY()); + cardInfo->set_face_down(card->getFaceDown()); + cardInfo->set_tapped(card->getTapped()); + cardInfo->set_attacking(card->getAttacking()); + cardInfo->set_color(card->getColor().toStdString()); + cardInfo->set_pt(card->getPT().toStdString()); + cardInfo->set_annotation(card->getAnnotation().toStdString()); + cardInfo->set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + cardInfo->set_doesnt_untap(card->getDoesntUntap()); + + QMapIterator cardCounterIterator(card->getCounters()); + while (cardCounterIterator.hasNext()) { + cardCounterIterator.next(); + ServerInfo_CardCounter *counterInfo = cardInfo->add_counter_list(); + counterInfo->set_id(cardCounterIterator.key()); + counterInfo->set_value(cardCounterIterator.value()); + } + + if (card->getParentCard()) { + cardInfo->set_attach_player_id(card->getParentCard()->getZone()->getPlayer()->getPlayerId()); + cardInfo->set_attach_zone(card->getParentCard()->getZone()->getName().toStdString()); + cardInfo->set_attach_card_id(card->getParentCard()->getId()); + } + } + } + if (zone->getType() == ServerInfo_Zone::HiddenZone) { + zone->setCardsBeingLookedAt(numberCards); + + Event_DumpZone event; + event.set_zone_owner_id(otherPlayer->getPlayerId()); + event.set_zone_name(zone->getName().toStdString()); + event.set_number_cards(numberCards); + event.set_is_reversed(cmd.is_reversed()); + ges.enqueueGameEvent(event, playerId); + } + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode +Server_AbstractPlayer::cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (cmd.has_player_id()) { + Server_AbstractPlayer *otherPlayer = game->getPlayer(cmd.player_id()); + if (!otherPlayer) + return Response::RespNameNotFound; + } + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + QList cardsToReveal; + if (cmd.top_cards() != -1) { + for (int i = 0; i < cmd.top_cards(); i++) { + Server_Card *card = zone->getCard(i); + if (!card) { + return Response::RespNameNotFound; + } + cardsToReveal.append(card); + } + } else if (cmd.card_id_size() == 0) { + cardsToReveal = zone->getCards(); + } else if (cmd.card_id_size() == 1 && cmd.card_id(0) == -2) { + // If there is a single card_id with value -2 (ie + // Player::RANDOM_CARD_FROM_ZONE), pick a random card. + // + // This is to be compatible with clients supporting a single card_id + // value, which send value -2 to request a random card. + if (zone->getCards().isEmpty()) { + return Response::RespContextError; + } + + cardsToReveal.append(zone->getCards().at(rng->rand(0, zone->getCards().size() - 1))); + } else { + for (auto cardId : cmd.card_id()) { + Server_Card *card = zone->getCard(cardId); + if (!card) { + return Response::RespNameNotFound; + } + cardsToReveal.append(card); + } + } + + Event_RevealCards eventOthers; + eventOthers.set_grant_write_access(cmd.grant_write_access()); + eventOthers.set_zone_name(zone->getName().toStdString()); + eventOthers.set_number_of_cards(cardsToReveal.size()); + for (auto cardId : cmd.card_id()) { + eventOthers.add_card_id(cardId); + } + if (cmd.has_player_id()) { + eventOthers.set_other_player_id(cmd.player_id()); + } + + Event_RevealCards eventPrivate(eventOthers); + + for (auto card : cardsToReveal) { + ServerInfo_Card *cardInfo = eventPrivate.add_cards(); + + cardInfo->set_id(card->getId()); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + cardInfo->set_name(card->getName().toStdString()); + cardInfo->set_x(card->getX()); + cardInfo->set_y(card->getY()); + cardInfo->set_face_down(card->getFaceDown()); + cardInfo->set_tapped(card->getTapped()); + cardInfo->set_attacking(card->getAttacking()); + cardInfo->set_color(card->getColor().toStdString()); + cardInfo->set_pt(card->getPT().toStdString()); + cardInfo->set_annotation(card->getAnnotation().toStdString()); + cardInfo->set_destroy_on_zone_change(card->getDestroyOnZoneChange()); + cardInfo->set_doesnt_untap(card->getDoesntUntap()); + + QMapIterator cardCounterIterator(card->getCounters()); + while (cardCounterIterator.hasNext()) { + cardCounterIterator.next(); + ServerInfo_CardCounter *counterInfo = cardInfo->add_counter_list(); + counterInfo->set_id(cardCounterIterator.key()); + counterInfo->set_value(cardCounterIterator.value()); + } + + if (card->getParentCard()) { + cardInfo->set_attach_player_id(card->getParentCard()->getZone()->getPlayer()->getPlayerId()); + cardInfo->set_attach_zone(card->getParentCard()->getZone()->getName().toStdString()); + cardInfo->set_attach_card_id(card->getParentCard()->getId()); + } + } + + if (cmd.has_player_id()) { + if (cmd.grant_write_access()) { + zone->addWritePermission(cmd.player_id()); + } + + if (isJudge()) { + ges.setOverwriteOwnership(true); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, cmd.player_id()); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + } else { + if (cmd.grant_write_access()) { + const QList &participantIds = game->getParticipants().keys(); + for (int anyParticipantId : participantIds) { + zone->addWritePermission(anyParticipantId); + } + } + + ges.enqueueGameEvent(eventPrivate, playerId); + } + + return Response::RespOk; +} + +Response::ResponseCode Server_AbstractPlayer::cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer & /* rc */, + GameEventStorage &ges) +{ + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + Event_ChangeZoneProperties event; + event.set_zone_name(cmd.zone_name()); + + // 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); + return Response::RespOk; +} + +void Server_AbstractPlayer::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo) +{ + getProperties(*info->mutable_properties(), withUserInfo); + if (recipient == this) { + if (deck) { + info->set_deck_list(deck->writeToString_Native().toStdString()); + } + } + + for (Server_Arrow *arrow : arrows) { + arrow->getInfo(info->add_arrow_list()); + } + + for (Server_CardZone *zone : zones) { + zone->getInfo(info->add_zone_list(), recipient, omniscient); + } +} + +void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &result) +{ + result.set_conceded(conceded); + result.set_sideboard_locked(sideboardLocked); + result.set_ready_start(readyStart); + if (deck) { + result.set_deck_hash(deck->getDeckHash().toStdString()); + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h new file mode 100644 index 000000000..9d9809298 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.h @@ -0,0 +1,164 @@ +#ifndef ABSTRACT_PLAYER_H +#define ABSTRACT_PLAYER_H + +#include "../serverinfo_user_container.h" +#include "server_abstract_participant.h" + +#include +#include + +class CardToMove; +class DeckList; +class Server_Arrow; +class Server_Card; +class Server_CardZone; +class Server_Counter; +struct MoveCardStruct; + +class Server_AbstractPlayer : public Server_AbstractParticipant +{ + Q_OBJECT +private: + class MoveCardCompareFunctor; + QMap arrows; + + void sendCreateTokenEvents(Server_CardZone *zone, Server_Card *card, int xCoord, int yCoord, GameEventStorage &ges); + void getPlayerProperties(ServerInfo_PlayerProperties &result) override; + +protected: + bool conceded; + DeckList *deck; + bool sideboardLocked; + QMap zones; + bool readyStart; + int nextCardId; + + void revealTopCardIfNeeded(Server_CardZone *zone, GameEventStorage &ges); + +public: + Server_AbstractPlayer(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_AbstractPlayer() override; + void prepareDestroy() override; + const DeckList *getDeckList() const + { + return deck; + } + bool getReadyStart() const + { + return readyStart; + } + void setReadyStart(bool _readyStart) + { + readyStart = _readyStart; + } + bool getConceded() const + { + return conceded; + } + void setConceded(bool _conceded) + { + conceded = _conceded; + } + + const QMap &getZones() const + { + return zones; + } + const QMap &getArrows() const + { + return arrows; + } + + int newCardId(); + int newArrowId() const; + + void addZone(Server_CardZone *zone); + void addArrow(Server_Arrow *arrow); + void updateArrowId(int id); + bool deleteArrow(int arrowId); + + virtual void setupZones(); + virtual void clearZones(); + + Response::ResponseCode moveCard(GameEventStorage &ges, + Server_CardZone *startzone, + const QList &_cards, + Server_CardZone *targetzone, + int xCoord, + int yCoord, + bool fixFreeSpaces = true, + bool undoingDraw = false, + bool isReversed = false); + + void processMoveCard(GameEventStorage &ges, + Server_CardZone *startzone, + Server_CardZone *targetzone, + MoveCardStruct cardStruct, + int xCoord, + int yCoord, + int &xIndex, + bool &revealTopStart, + bool &revealTopTarget, + bool isReversed, + bool undoingDraw); + + virtual void onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw); + + void unattachCard(GameEventStorage &ges, Server_Card *card); + Response::ResponseCode setCardAttrHelper(GameEventStorage &ges, + int targetPlayerId, + const QString &zone, + int cardId, + CardAttribute attribute, + const QString &attrValue, + Server_Card *unzonedCard = nullptr); + + virtual Response::ResponseCode + cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdRollDie(const Command_RollDie &cmd, ResponseContainer &rc, GameEventStorage &ges) const override; + virtual Response::ResponseCode + cmdMoveCard(const Command_MoveCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdFlipCard(const Command_FlipCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdAttachCard(const Command_AttachCard &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdCreateToken(const Command_CreateToken &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdCreateArrow(const Command_CreateArrow &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdDeleteArrow(const Command_DeleteArrow &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdSetCardAttr(const Command_SetCardAttr &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdSetCardCounter(const Command_SetCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdIncCardCounter(const Command_IncCardCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdDumpZone(const Command_DumpZone &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode + cmdRevealCards(const Command_RevealCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + virtual Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) override; + + virtual void getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *playerWhosAsking, + bool omniscient, + bool withUserInfo) override; +}; + +#endif diff --git a/common/server_arrow.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp similarity index 63% rename from common/server_arrow.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp index fe02995d3..f6787baa2 100644 --- a/common/server_arrow.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.cpp @@ -1,12 +1,19 @@ #include "server_arrow.h" -#include "pb/serverinfo_arrow.pb.h" #include "server_card.h" #include "server_cardzone.h" #include "server_player.h" -Server_Arrow::Server_Arrow(int _id, Server_Card *_startCard, Server_ArrowTarget *_targetItem, const color &_arrowColor) - : id(_id), startCard(_startCard), targetItem(_targetItem), arrowColor(_arrowColor) +#include + +Server_Arrow::Server_Arrow(int _id, + Server_Card *_startCard, + Server_ArrowTarget *_targetItem, + const color &_arrowColor, + int _phaseCreated, + int _phaseDeleted) + : id(_id), startCard(_startCard), targetItem(_targetItem), arrowColor(_arrowColor), phaseCreated(_phaseCreated), + phaseDeleted(_phaseDeleted) { } @@ -18,7 +25,7 @@ void Server_Arrow::getInfo(ServerInfo_Arrow *info) info->set_start_card_id(startCard->getId()); info->mutable_arrow_color()->CopyFrom(arrowColor); - Server_Card *targetCard = qobject_cast(targetItem); + auto *targetCard = qobject_cast(targetItem); if (targetCard) { info->set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerId()); info->set_target_zone(targetCard->getZone()->getName().toStdString()); diff --git a/common/server_arrow.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h similarity index 62% rename from common/server_arrow.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h index 13a6ea2d8..1f302358b 100644 --- a/common/server_arrow.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrow.h @@ -1,7 +1,7 @@ #ifndef SERVER_ARROW_H #define SERVER_ARROW_H -#include "pb/color.pb.h" +#include class Server_Card; class Server_ArrowTarget; @@ -14,9 +14,15 @@ private: Server_Card *startCard; Server_ArrowTarget *targetItem; color arrowColor; + int phaseCreated, phaseDeleted; public: - Server_Arrow(int _id, Server_Card *_startCard, Server_ArrowTarget *_targetItem, const color &_arrowColor); + Server_Arrow(int _id, + Server_Card *_startCard, + Server_ArrowTarget *_targetItem, + const color &_arrowColor, + int _phaseCreated, + int _phaseDeleted); int getId() const { return id; @@ -45,6 +51,10 @@ public: { return arrowColor; } + bool checkPhaseDeletion(int phase) const // returns true if the arrow should be deleted in this phase + { + return phase < phaseCreated || phase >= phaseDeleted; + } void getInfo(ServerInfo_Arrow *info); }; diff --git a/common/server_arrowtarget.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.cpp similarity index 100% rename from common/server_arrowtarget.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.cpp diff --git a/common/server_arrowtarget.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.h similarity index 100% rename from common/server_arrowtarget.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_arrowtarget.h diff --git a/common/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp similarity index 96% rename from common/server_card.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index 323b4d827..86ff2f008 100644 --- a/common/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -19,13 +19,13 @@ ***************************************************************************/ #include "server_card.h" -#include "pb/event_set_card_attr.pb.h" -#include "pb/event_set_card_counter.pb.h" -#include "pb/serverinfo_card.pb.h" #include "server_cardzone.h" #include "server_player.h" #include +#include +#include +#include Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) : zone(_zone), id(_id), coord_x(_coord_x), coord_y(_coord_y), cardRef(cardRef), tapped(false), attacking(false), diff --git a/common/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h similarity index 97% rename from common/server_card.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h index d96b9d056..bc326bbc4 100644 --- a/common/server_card.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -20,13 +20,13 @@ #ifndef SERVER_CARD_H #define SERVER_CARD_H -#include "card_ref.h" -#include "pb/card_attributes.pb.h" -#include "pb/serverinfo_card.pb.h" #include "server_arrowtarget.h" #include #include +#include +#include +#include class Server_CardZone; class Event_SetCardCounter; diff --git a/common/server_cardzone.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp similarity index 94% rename from common/server_cardzone.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp index 88b61622f..f2a35e548 100644 --- a/common/server_cardzone.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.cpp @@ -19,15 +19,15 @@ ***************************************************************************/ #include "server_cardzone.h" -#include "pb/command_move_card.pb.h" -#include "rng_abstract.h" +#include "server_abstract_player.h" #include "server_card.h" -#include "server_player.h" #include #include +#include +#include -Server_CardZone::Server_CardZone(Server_Player *_player, +Server_CardZone::Server_CardZone(Server_AbstractPlayer *_player, const QString &_name, bool _has_coords, ServerInfo_Zone::ZoneType _type) @@ -63,11 +63,7 @@ void Server_CardZone::shuffle(int start, int end) for (int i = end; i > start; i--) { int j = rng->rand(start, i); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)) cards.swapItemsAt(j, i); -#else - cards.swap(j, i); -#endif } playersWithWritePermission.clear(); } @@ -318,7 +314,7 @@ void Server_CardZone::addWritePermission(int playerId) playersWithWritePermission.insert(playerId); } -void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_Player *playerWhosAsking, bool omniscient) +void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_AbstractParticipant *recipient, bool omniscient) { info->set_name(name.toStdString()); info->set_type(type); @@ -327,10 +323,10 @@ void Server_CardZone::getInfo(ServerInfo_Zone *info, Server_Player *playerWhosAs info->set_always_reveal_top_card(alwaysRevealTopCard); info->set_always_look_at_top_card(alwaysLookAtTopCard); - const auto selfPlayerAsking = playerWhosAsking == player || omniscient; - const auto zonesSelfCanSee = type != ServerInfo_Zone::HiddenZone; - const auto otherPlayerAsking = playerWhosAsking != player; - const auto zonesOthersCanSee = type == ServerInfo_Zone::PublicZone; + const bool selfPlayerAsking = recipient == player || omniscient; + const bool zonesSelfCanSee = type != ServerInfo_Zone::HiddenZone; + const bool otherPlayerAsking = recipient != player; + const bool zonesOthersCanSee = type == ServerInfo_Zone::PublicZone; if ((selfPlayerAsking && zonesSelfCanSee) || (otherPlayerAsking && zonesOthersCanSee)) { QListIterator cardIterator(cards); while (cardIterator.hasNext()) diff --git a/common/server_cardzone.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h similarity index 74% rename from common/server_cardzone.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h index 51aefbafd..77fea54ca 100644 --- a/common/server_cardzone.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_cardzone.h @@ -20,22 +20,21 @@ #ifndef SERVER_CARDZONE_H #define SERVER_CARDZONE_H -#include "pb/serverinfo_zone.pb.h" - -#include #include #include #include +#include class Server_Card; -class Server_Player; +class Server_AbstractPlayer; +class Server_AbstractParticipant; class Server_Game; class GameEventStorage; class Server_CardZone { private: - Server_Player *player; + Server_AbstractPlayer *player; QString name; bool has_coords; // having coords means this zone has x and y coordinates ServerInfo_Zone::ZoneType type; @@ -51,10 +50,13 @@ private: void insertCardIntoCoordMap(Server_Card *card, int x, int y); public: - Server_CardZone(Server_Player *_player, const QString &_name, bool _has_coords, ServerInfo_Zone::ZoneType _type); + Server_CardZone(Server_AbstractPlayer *_player, + const QString &_name, + bool _has_coords, + ServerInfo_Zone::ZoneType _type); ~Server_CardZone(); - const QList &getCards() const + [[nodiscard]] const QList &getCards() const { return cards; } @@ -62,7 +64,7 @@ public: int removeCard(Server_Card *card, bool &wasLookedAt); Server_Card *getCard(int id, int *position = nullptr, bool remove = false); - int getCardsBeingLookedAt() const + [[nodiscard]] int getCardsBeingLookedAt() const { return cardsBeingLookedAt; } @@ -70,28 +72,28 @@ public: { cardsBeingLookedAt = qMax(0, _cardsBeingLookedAt); } - bool isCardAtPosLookedAt(int pos) const; - bool hasCoords() const + [[nodiscard]] bool isCardAtPosLookedAt(int pos) const; + [[nodiscard]] bool hasCoords() const { return has_coords; } - ServerInfo_Zone::ZoneType getType() const + [[nodiscard]] ServerInfo_Zone::ZoneType getType() const { return type; } - QString getName() const + [[nodiscard]] QString getName() const { return name; } - Server_Player *getPlayer() const + [[nodiscard]] Server_AbstractPlayer *getPlayer() const { return player; } - void getInfo(ServerInfo_Zone *info, Server_Player *playerWhosAsking, bool omniscient); + void getInfo(ServerInfo_Zone *info, Server_AbstractParticipant *recipient, bool omniscient); - int getFreeGridColumn(int x, int y, const QString &cardName, bool dontStackSameName) const; - bool isColumnEmpty(int x, int y) const; - bool isColumnStacked(int x, int y) const; + [[nodiscard]] int getFreeGridColumn(int x, int y, const QString &cardName, bool dontStackSameName) const; + [[nodiscard]] bool isColumnEmpty(int x, int y) const; + [[nodiscard]] bool isColumnStacked(int x, int y) const; void fixFreeSpaces(GameEventStorage &ges); void moveCardInRow(GameEventStorage &ges, Server_Card *card, int x, int y); void insertCard(Server_Card *card, int x, int y); @@ -99,11 +101,11 @@ public: void shuffle(int start = 0, int end = -1); void clear(); void addWritePermission(int playerId); - const QSet &getPlayersWithWritePermission() const + [[nodiscard]] const QSet &getPlayersWithWritePermission() const { return playersWithWritePermission; } - bool getAlwaysRevealTopCard() const + [[nodiscard]] bool getAlwaysRevealTopCard() const { return alwaysRevealTopCard; } @@ -111,7 +113,7 @@ public: { alwaysRevealTopCard = _alwaysRevealTopCard; } - bool getAlwaysLookAtTopCard() const + [[nodiscard]] bool getAlwaysLookAtTopCard() const { return alwaysLookAtTopCard; } diff --git a/common/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp similarity index 88% rename from common/server_counter.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index 3dc5c6d66..b18e11c2b 100644 --- a/common/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -1,6 +1,6 @@ #include "server_counter.h" -#include "pb/serverinfo_counter.pb.h" +#include Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) diff --git a/common/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h similarity index 97% rename from common/server_counter.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 3a5f837ae..55aad991c 100644 --- a/common/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -20,9 +20,8 @@ #ifndef SERVER_COUNTER_H #define SERVER_COUNTER_H -#include "pb/color.pb.h" - #include +#include class ServerInfo_Counter; diff --git a/common/server_game.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp similarity index 67% rename from common/server_game.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp index 95d97f1da..2224ddb13 100644 --- a/common/server_game.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.cpp @@ -19,36 +19,39 @@ ***************************************************************************/ #include "server_game.h" -#include "decklist.h" -#include "pb/context_connection_state_changed.pb.h" -#include "pb/context_deck_select.pb.h" -#include "pb/context_ping_changed.pb.h" -#include "pb/event_delete_arrow.pb.h" -#include "pb/event_game_closed.pb.h" -#include "pb/event_game_host_changed.pb.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_game_state_changed.pb.h" -#include "pb/event_join.pb.h" -#include "pb/event_kicked.pb.h" -#include "pb/event_leave.pb.h" -#include "pb/event_player_properties_changed.pb.h" -#include "pb/event_replay_added.pb.h" -#include "pb/event_set_active_phase.pb.h" -#include "pb/event_set_active_player.pb.h" -#include "pb/game_replay.pb.h" -#include "pb/serverinfo_playerping.pb.h" -#include "server.h" +#include "../server.h" +#include "../server_database_interface.h" +#include "../server_protocolhandler.h" +#include "../server_room.h" +#include "libcockatrice/protocol/pb/command_move_card.pb.h" +#include "server_abstract_player.h" #include "server_arrow.h" #include "server_card.h" #include "server_cardzone.h" -#include "server_database_interface.h" #include "server_player.h" -#include "server_protocolhandler.h" -#include "server_room.h" +#include "server_spectator.h" #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, int _gameId, @@ -73,11 +76,7 @@ Server_Game::Server_Game(const ServerInfo_User &_creatorInfo, spectatorsSeeEverything(_spectatorsSeeEverything), startingLifeTotal(_startingLifeTotal), shareDecklistsOnLoad(_shareDecklistsOnLoad), inactivityCounter(0), startTimeOfThisGame(0), secondsElapsed(0), firstGameStarted(false), turnOrderReversed(false), startTime(QDateTime::currentDateTime()), pingClock(nullptr), -#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()); @@ -101,10 +100,10 @@ Server_Game::~Server_Game() gameClosed = true; sendGameEventContainer(prepareGameEvent(Event_GameClosed(), -1)); - for (auto *player : players.values()) { - player->prepareDestroy(); + for (auto *participant : participants.values()) { + participant->prepareDestroy(); } - players.clear(); + participants.clear(); room->removeGame(this); delete creatorInfo; @@ -189,36 +188,23 @@ void Server_Game::pingClockTimeout() bool allPlayersInactive = true; int playerCount = 0; - for (auto *player : players) { - if (player == nullptr) + for (auto *participant : participants) { + if (participant == nullptr) continue; - if (!player->getSpectator()) { + if (!participant->isSpectator()) { ++playerCount; } - int oldPingTime = player->getPingTime(); - int newPingTime; - { - QMutexLocker playerMutexLocker(&player->playerMutex); - if (player->getUserInterface()) { - newPingTime = player->getUserInterface()->getLastCommandTime(); - } else { - newPingTime = -1; - } - } - - if ((newPingTime != -1) && (!player->getSpectator() || player->getPlayerId() == hostId)) { - allPlayersInactive = false; - } - - if ((abs(oldPingTime - newPingTime) > 1) || ((newPingTime == -1) && (oldPingTime != -1)) || - ((newPingTime != -1) && (oldPingTime == -1))) { - player->setPingTime(newPingTime); - + if (participant->updatePingTime()) { Event_PlayerPropertiesChanged event; - event.mutable_player_properties()->set_ping_seconds(newPingTime); - ges.enqueueGameEvent(event, player->getPlayerId()); + event.mutable_player_properties()->set_ping_seconds(participant->getPingTime()); + ges.enqueueGameEvent(event, participant->getPlayerId()); + } + + if ((participant->getPingTime() != -1) && + (!participant->isSpectator() || participant->getPlayerId() == hostId)) { + allPlayersInactive = false; } } ges.sendToGame(this); @@ -233,16 +219,32 @@ void Server_Game::pingClockTimeout() } } +QMap Server_Game::getPlayers() const // copies pointers to new map +{ + QMap players; + QMutexLocker locker(&gameMutex); + for (int id : participants.keys()) { + auto *participant = participants[id]; + if (!participant->isSpectator()) { + players[id] = static_cast(participant); + } + } + return players; +} + +Server_AbstractPlayer *Server_Game::getPlayer(int id) const +{ + auto *participant = participants.value(id); + if (participant && !participant->isSpectator()) { + return static_cast(participant); + } else { + return nullptr; + } +} + int Server_Game::getPlayerCount() const { - QMutexLocker locker(&gameMutex); - - int result = 0; - for (Server_Player *player : players.values()) { - if (!player->getSpectator()) - ++result; - } - return result; + return participants.size() - getSpectatorCount(); } int Server_Game::getSpectatorCount() const @@ -250,15 +252,15 @@ int Server_Game::getSpectatorCount() const QMutexLocker locker(&gameMutex); int result = 0; - for (Server_Player *player : players.values()) { - if (player->getSpectator()) + for (Server_AbstractParticipant *participant : participants.values()) { + if (participant->isSpectator()) ++result; } return result; } void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, - Server_Player *playerWhosAsking, + Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo) { @@ -270,8 +272,8 @@ void Server_Game::createGameStateChangedEvent(Event_GameStateChanged *event, } else event->set_game_started(false); - for (Server_Player *otherPlayer : players.values()) { - otherPlayer->getInfo(event->add_player_list(), playerWhosAsking, omniscient, withUserInfo); + for (Server_AbstractParticipant *participant : participants.values()) { + participant->getInfo(event->add_player_list(), recipient, omniscient, withUserInfo); } } @@ -294,21 +296,21 @@ void Server_Game::sendGameStateToPlayers() createGameStateChangedEvent(&spectatorNormalEvent, nullptr, false, false); // send game state info to clients according to their role in the game - for (Server_Player *player : players.values()) { + for (auto *participant : participants.values()) { GameEventContainer *gec; - if (player->getSpectator()) { - if (spectatorsSeeEverything || player->getJudge()) { + if (participant->isSpectator()) { + if (spectatorsSeeEverything || participant->isJudge()) { gec = prepareGameEvent(omniscientEvent, -1); } else { gec = prepareGameEvent(spectatorNormalEvent, -1); } } else { Event_GameStateChanged event; - createGameStateChangedEvent(&event, player, false, false); + createGameStateChangedEvent(&event, participant, false, false); gec = prepareGameEvent(event, -1); } - player->sendGameEvent(*gec); + participant->sendGameEvent(*gec); delete gec; } } @@ -322,26 +324,26 @@ void Server_Game::doStartGameIfReady(bool forceStartGame) return; } - for (Server_Player *player : players.values()) { - if (!player->getReadyStart() && !player->getSpectator()) { + auto players = getPlayers(); + for (auto *player : players.values()) { + if (!player->getReadyStart()) { if (forceStartGame) { // Player is not ready to start, so kick them // TODO: Move them to Spectators instead - kickPlayer(player->getPlayerId()); + kickParticipant(player->getPlayerId()); } else { return; } } } - for (Server_Player *player : players.values()) { - if (!player->getSpectator()) { - player->setupZones(); - } + players = getPlayers(); // players could have been kicked, get new list of players + for (Server_AbstractPlayer *player : players.values()) { + player->setupZones(); } gameStarted = true; - for (Server_Player *player : players.values()) { + for (auto *player : players.values()) { player->setConceded(false); player->setReadyStart(false); } @@ -392,8 +394,9 @@ void Server_Game::stopGameIfFinished() QMutexLocker locker(&gameMutex); int playing = 0; - for (Server_Player *player : players.values()) { - if (!player->getConceded() && !player->getSpectator()) + auto players = getPlayers(); + for (auto *player : players.values()) { + if (!player->getConceded()) ++playing; } if (playing > 1) @@ -401,7 +404,7 @@ void Server_Game::stopGameIfFinished() gameStarted = false; - for (Server_Player *player : players.values()) { + for (auto *player : players.values()) { player->clearZones(); player->setConceded(false); } @@ -424,8 +427,8 @@ Response::ResponseCode Server_Game::checkJoin(ServerInfo_User *user, bool asJudge) { Server_DatabaseInterface *databaseInterface = room->getServer()->getDatabaseInterface(); - for (Server_Player *player : players.values()) { - if (player->getUserInfo()->name() == user->name()) + for (auto *participant : participants.values()) { + if (participant->getUserInfo()->name() == user->name()) return Response::RespContextError; } @@ -459,8 +462,8 @@ bool Server_Game::containsUser(const QString &userName) const { QMutexLocker locker(&gameMutex); - for (Server_Player *player : players.values()) { - if (player->getUserInfo()->name() == userName.toStdString()) + for (auto *participant : participants.values()) { + if (participant->getUserInfo()->name() == userName.toStdString()) return true; } return false; @@ -474,26 +477,32 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, { QMutexLocker locker(&gameMutex); - Server_Player *newPlayer = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), - spectator, judge, userInterface); + Server_AbstractParticipant *newParticipant; + if (spectator) { + newParticipant = new Server_Spectator(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), + judge, userInterface); + } else { + newParticipant = new Server_Player(this, nextPlayerId++, userInterface->copyUserInfo(true, true, true), judge, + userInterface); + } - newPlayer->moveToThread(thread()); + newParticipant->moveToThread(thread()); Event_Join joinEvent; - newPlayer->getProperties(*joinEvent.mutable_player_properties(), true); + newParticipant->getProperties(*joinEvent.mutable_player_properties(), true); sendGameEventContainer(prepareGameEvent(joinEvent, -1)); - const QString playerName = QString::fromStdString(newPlayer->getUserInfo()->name()); - players.insert(newPlayer->getPlayerId(), newPlayer); + const QString playerName = QString::fromStdString(newParticipant->getUserInfo()->name()); + participants.insert(newParticipant->getPlayerId(), newParticipant); 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(); + //! \todo transferring host to spectators has side effects + if (newParticipant->getUserInfo()->name() == creatorInfo->name()) { + hostId = newParticipant->getPlayerId(); sendGameEventContainer(prepareGameEvent(Event_GameHostChanged(), hostId)); } } @@ -507,41 +516,42 @@ void Server_Game::addPlayer(Server_AbstractUserInterface *userInterface, emit gameInfoChanged(gameInfo); } - if ((newPlayer->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) - room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newPlayer->getPlayerId()); + if ((newParticipant->getUserInfo()->user_level() & ServerInfo_User::IsRegistered) && !spectator) + room->getServer()->addPersistentPlayer(playerName, room->getId(), gameId, newParticipant->getPlayerId()); - userInterface->playerAddedToGame(gameId, room->getId(), newPlayer->getPlayerId()); + userInterface->playerAddedToGame(gameId, room->getId(), newParticipant->getPlayerId()); - createGameJoinedEvent(newPlayer, rc, false); + createGameJoinedEvent(newParticipant, rc, false); } -void Server_Game::removePlayer(Server_Player *player, Event_Leave::LeaveReason reason) +void Server_Game::removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason) { - room->getServer()->removePersistentPlayer(QString::fromStdString(player->getUserInfo()->name()), room->getId(), - gameId, player->getPlayerId()); - players.remove(player->getPlayerId()); + room->getServer()->removePersistentPlayer(QString::fromStdString(participant->getUserInfo()->name()), room->getId(), + gameId, participant->getPlayerId()); + participants.remove(participant->getPlayerId()); + bool spectator = participant->isSpectator(); GameEventStorage ges; - removeArrowsRelatedToPlayer(ges, player); - unattachCards(ges, player); + if (!spectator) { + auto *player = static_cast(participant); + removeArrowsRelatedToPlayer(ges, player); + unattachCards(ges, player); + } Event_Leave event; event.set_reason(reason); - ges.enqueueGameEvent(event, player->getPlayerId()); + ges.enqueueGameEvent(event, participant->getPlayerId()); ges.sendToGame(this); - bool playerActive = activePlayer == player->getPlayerId(); - bool playerHost = hostId == player->getPlayerId(); - bool spectator = player->getSpectator(); - player->prepareDestroy(); + bool playerActive = activePlayer == participant->getPlayerId(); + bool playerHost = hostId == participant->getPlayerId(); + participant->prepareDestroy(); if (playerHost) { int newHostId = -1; - for (Server_Player *otherPlayer : players.values()) { - if (!otherPlayer->getSpectator()) { - newHostId = otherPlayer->getPlayerId(); - break; - } + for (auto *otherPlayer : getPlayers().values()) { + newHostId = otherPlayer->getPlayerId(); + break; } if (newHostId != -1) { hostId = newHostId; @@ -566,40 +576,39 @@ void Server_Game::removePlayer(Server_Player *player, Event_Leave::LeaveReason r emit gameInfoChanged(gameInfo); } -void Server_Game::removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_Player *player) +void Server_Game::removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player) { QMutexLocker locker(&gameMutex); // 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. - for (Server_Player *otherPlayer : players.values()) { - QList arrows = otherPlayer->getArrows().values(); + for (Server_AbstractPlayer *anyPlayer : getPlayers().values()) { QList toDelete; - for (int i = 0; i < arrows.size(); ++i) { - Server_Arrow *a = arrows[i]; - Server_Card *targetCard = qobject_cast(a->getTargetItem()); + for (auto *arrow : anyPlayer->getArrows().values()) { + auto *targetCard = qobject_cast(arrow->getTargetItem()); if (targetCard) { if (targetCard->getZone() != nullptr && targetCard->getZone()->getPlayer() == player) - toDelete.append(a); - } else if (static_cast(a->getTargetItem()) == player) - toDelete.append(a); + toDelete.append(arrow); + } else if (arrow->getTargetItem() == player) { + toDelete.append(arrow); + } // Don't use else here! It has to happen regardless of whether targetCard == 0. - if (a->getStartCard()->getZone() != nullptr && a->getStartCard()->getZone()->getPlayer() == player) - toDelete.append(a); + if (arrow->getStartCard()->getZone() != nullptr && arrow->getStartCard()->getZone()->getPlayer() == player) + toDelete.append(arrow); } - for (int i = 0; i < toDelete.size(); ++i) { + for (auto *arrow : toDelete) { Event_DeleteArrow event; - event.set_arrow_id(toDelete[i]->getId()); - ges.enqueueGameEvent(event, otherPlayer->getPlayerId()); + event.set_arrow_id(arrow->getId()); + ges.enqueueGameEvent(event, anyPlayer->getPlayerId()); - otherPlayer->deleteArrow(toDelete[i]->getId()); + anyPlayer->deleteArrow(arrow->getId()); } } } -void Server_Game::unattachCards(GameEventStorage &ges, Server_Player *player) +void Server_Game::unattachCards(GameEventStorage &ges, Server_AbstractPlayer *player) { QMutexLocker locker(&gameMutex); @@ -621,19 +630,19 @@ void Server_Game::unattachCards(GameEventStorage &ges, Server_Player *player) } } -bool Server_Game::kickPlayer(int playerId) +bool Server_Game::kickParticipant(int playerId) { QMutexLocker locker(&gameMutex); - Server_Player *playerToKick = players.value(playerId); - if (!playerToKick) + auto *participant = participants.value(playerId); + if (!participant) return false; GameEventContainer *gec = prepareGameEvent(Event_Kicked(), -1); - playerToKick->sendGameEvent(*gec); + participant->sendGameEvent(*gec); delete gec; - removePlayer(playerToKick, Event_Leave::USER_KICKED); + removeParticipant(participant, Event_Leave::USER_KICKED); return true; } @@ -642,6 +651,8 @@ void Server_Game::setActivePlayer(int _activePlayer) { QMutexLocker locker(&gameMutex); + removeArrows(0, true); + activePlayer = _activePlayer; Event_SetActivePlayer event; @@ -651,43 +662,50 @@ void Server_Game::setActivePlayer(int _activePlayer) setActivePhase(0); } -void Server_Game::setActivePhase(int _activePhase) +void Server_Game::setActivePhase(int newPhase) { QMutexLocker locker(&gameMutex); - for (Server_Player *player : players.values()) { - QList toDelete = player->getArrows().values(); - for (int i = 0; i < toDelete.size(); ++i) { - Server_Arrow *a = toDelete[i]; - - Event_DeleteArrow event; - event.set_arrow_id(a->getId()); - sendGameEventContainer(prepareGameEvent(event, player->getPlayerId())); - - player->deleteArrow(a->getId()); - } - } - - activePhase = _activePhase; + removeArrows(newPhase); + activePhase = newPhase; Event_SetActivePhase event; event.set_phase(activePhase); sendGameEventContainer(prepareGameEvent(event, -1)); } +void Server_Game::removeArrows(int newPhase, bool force) +{ + QMutexLocker locker(&gameMutex); + + for (auto *anyPlayer : getPlayers().values()) { + for (auto *arrowToDelete : anyPlayer->getArrows().values()) { // values creates a copy + if (force || arrowToDelete->checkPhaseDeletion(newPhase)) { + Event_DeleteArrow event; + event.set_arrow_id(arrowToDelete->getId()); + sendGameEventContainer(prepareGameEvent(event, anyPlayer->getPlayerId())); + + anyPlayer->deleteArrow(arrowToDelete->getId()); + } + } + } +} + void Server_Game::nextTurn() { QMutexLocker locker(&gameMutex); - if (players.isEmpty()) { + if (participants.isEmpty()) { qWarning() << "Server_Game::nextTurn was called while players is empty; gameId = " << gameId; return; } + auto players = getPlayers(); const QList keys = players.keys(); int listPos = -1; - if (activePlayer != -1) + if (activePlayer != -1) { listPos = keys.indexOf(activePlayer); + } do { if (turnOrderReversed) { --listPos; @@ -700,19 +718,21 @@ void Server_Game::nextTurn() listPos = 0; } } - } while (players.value(keys[listPos])->getSpectator() || players.value(keys[listPos])->getConceded()); + } while (players.value(keys[listPos])->getConceded()); setActivePlayer(keys[listPos]); } -void Server_Game::createGameJoinedEvent(Server_Player *player, ResponseContainer &rc, bool resuming) +void Server_Game::createGameJoinedEvent(Server_AbstractParticipant *joiningParticipant, + ResponseContainer &rc, + bool resuming) { Event_GameJoined event1; getInfo(*event1.mutable_game_info()); event1.set_host_id(hostId); - event1.set_player_id(player->getPlayerId()); - event1.set_spectator(player->getSpectator()); - event1.set_judge(player->getJudge()); + event1.set_player_id(joiningParticipant->getPlayerId()); + event1.set_spectator(joiningParticipant->isSpectator()); + event1.set_judge(joiningParticipant->isJudge()); event1.set_resuming(resuming); if (resuming) { const QStringList &allGameTypes = room->getGameTypes(); @@ -730,9 +750,9 @@ void Server_Game::createGameJoinedEvent(Server_Player *player, ResponseContainer event2.set_active_player_id(activePlayer); event2.set_active_phase(activePhase); - for (auto *_player : players.values()) { - _player->getInfo(event2.add_player_list(), _player, - (_player->getSpectator() && (spectatorsSeeEverything || _player->getJudge())), true); + bool omniscient = joiningParticipant->isSpectator() && (spectatorsSeeEverything || joiningParticipant->isJudge()); + for (auto *participant : participants.values()) { + participant->getInfo(event2.add_player_list(), joiningParticipant, omniscient, true); } rc.enqueuePostResponseItem(ServerMessage::GAME_EVENT_CONTAINER, prepareGameEvent(event2, -1)); @@ -745,12 +765,12 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, QMutexLocker locker(&gameMutex); cont->set_game_id(gameId); - for (Server_Player *player : players.values()) { - const bool playerPrivate = (player->getPlayerId() == privatePlayerId) || - (player->getSpectator() && (spectatorsSeeEverything || player->getJudge())); + for (auto *participant : participants.values()) { + const bool playerPrivate = (participant->getPlayerId() == privatePlayerId) || + (participant->isSpectator() && (spectatorsSeeEverything || participant->isJudge())); if ((recipients.testFlag(GameEventStorageItem::SendToPrivate) && playerPrivate) || (recipients.testFlag(GameEventStorageItem::SendToOthers) && !playerPrivate)) - player->sendGameEvent(*cont); + participant->sendGameEvent(*cont); } if (recipients.testFlag(GameEventStorageItem::SendToPrivate)) { cont->set_seconds_elapsed(secondsElapsed - startTimeOfThisGame); @@ -764,7 +784,7 @@ void Server_Game::sendGameEventContainer(GameEventContainer *cont, GameEventContainer * Server_Game::prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context) { - GameEventContainer *cont = new GameEventContainer; + auto *cont = new GameEventContainer; cont->set_game_id(gameId); if (context) cont->mutable_context()->CopyFrom(*context); @@ -807,3 +827,46 @@ void Server_Game::getInfo(ServerInfo_Game &result) const result.set_start_time(startTime.toSecsSinceEpoch()); } } + +void Server_Game::returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player) +{ + QMutexLocker locker(&gameMutex); + // Return cards to their rightful owners before conceding the game + static const QRegularExpression ownerRegex{"Owner: ?([^\n]+)"}; + const auto &playerTable = player->getZones().value(ZoneNames::TABLE); + for (const auto &card : playerTable->getCards()) { + if (card == nullptr) { + continue; + } + + const auto ®exResult = ownerRegex.match(card->getAnnotation()); + if (!regexResult.hasMatch()) { + continue; + } + + CardToMove cardToMove; + cardToMove.set_card_id(card->getId()); + + for (const auto *otherPlayer : getPlayers()) { + if (otherPlayer == nullptr || otherPlayer->getUserInfo() == nullptr) { + continue; + } + + const auto &ownerToReturnTo = regexResult.captured(1); + const auto &correctOwner = QString::compare(QString::fromStdString(otherPlayer->getUserInfo()->name()), + ownerToReturnTo, Qt::CaseInsensitive) == 0; + if (!correctOwner) { + continue; + } + + const auto &targetZone = otherPlayer->getZones().value(ZoneNames::TABLE); + + if (playerTable == nullptr || targetZone == nullptr) { + continue; + } + + player->moveCard(ges, playerTable, QList() << &cardToMove, targetZone, 0, 0, false); + break; + } + } +} diff --git a/common/server_game.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h similarity index 84% rename from common/server_game.h rename to libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h index 226a14149..1c658f2ba 100644 --- a/common/server_game.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_game.h @@ -20,10 +20,7 @@ #ifndef SERVERGAME_H #define SERVERGAME_H -#include "pb/event_leave.pb.h" -#include "pb/response.pb.h" -#include "pb/serverinfo_game.pb.h" -#include "server_response_containers.h" +#include "../server_response_containers.h" #include #include @@ -31,14 +28,17 @@ #include #include #include +#include +#include +#include class QTimer; class GameEventContainer; class GameReplay; class Server_Room; -class Server_Player; +class Server_AbstractPlayer; +class Server_AbstractParticipant; class ServerInfo_User; -class ServerInfo_Player; class ServerInfo_Game; class Server_AbstractUserInterface; class Event_GameStateChanged; @@ -51,7 +51,7 @@ private: int nextPlayerId; int hostId; ServerInfo_User *creatorInfo; - QMap players; + QMap participants; QSet allPlayersEver, allSpectatorsEver; bool gameStarted; bool gameClosed; @@ -78,7 +78,7 @@ private: GameReplay *currentReplay; void createGameStateChangedEvent(Event_GameStateChanged *event, - Server_Player *playerWhosAsking, + Server_AbstractParticipant *recipient, bool omniscient, bool withUserInfo); void storeGameInformation(); @@ -90,11 +90,7 @@ private slots: void doStartGameIfReady(bool forceStartGame = false); 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, @@ -130,9 +126,11 @@ public: } int getPlayerCount() const; int getSpectatorCount() const; - const QMap &getPlayers() const + QMap getPlayers() const; + Server_AbstractPlayer *getPlayer(int id) const; + const QMap &getParticipants() const { - return players; + return participants; } int getGameId() const { @@ -182,10 +180,10 @@ public: bool spectator, bool judge, bool broadcastUpdate = true); - void removePlayer(Server_Player *player, Event_Leave::LeaveReason reason); - void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_Player *player); - void unattachCards(GameEventStorage &ges, Server_Player *player); - bool kickPlayer(int playerId); + void removeParticipant(Server_AbstractParticipant *participant, Event_Leave::LeaveReason reason); + void removeArrowsRelatedToPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); + void unattachCards(GameEventStorage &ges, Server_AbstractPlayer *player); + bool kickParticipant(int playerId); void startGameIfReady(bool forceStartGame); void stopGameIfFinished(); int getActivePlayer() const @@ -196,8 +194,9 @@ public: { return activePhase; } - void setActivePlayer(int _activePlayer); - void setActivePhase(int _activePhase); + void setActivePlayer(int newPlayer); + void setActivePhase(int newPhase); + void removeArrows(int newPhase, bool force = false); void nextTurn(); int getSecondsElapsed() const { @@ -208,7 +207,7 @@ public: return turnOrderReversed = !turnOrderReversed; } - void createGameJoinedEvent(Server_Player *player, ResponseContainer &rc, bool resuming); + void createGameJoinedEvent(Server_AbstractParticipant *participant, ResponseContainer &rc, bool resuming); GameEventContainer * prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context = 0); @@ -219,6 +218,7 @@ public: GameEventStorageItem::EventRecipients recipients = GameEventStorageItem::SendToPrivate | GameEventStorageItem::SendToOthers, int privatePlayerId = -1); + void returnCardsFromPlayer(GameEventStorage &ges, Server_AbstractPlayer *player); }; #endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h new file mode 100644 index 000000000..c5b293b6d --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_move_card_struct.h @@ -0,0 +1,26 @@ +#ifndef MOVE_CARD_STRUCT +#define MOVE_CARD_STRUCT + +#include "server_card.h" +class CardToMove; + +struct MoveCardStruct +{ + Server_Card *card; + int position; + const CardToMove *cardToMove; + int xCoord, yCoord; + MoveCardStruct(Server_Card *_card, int _position, const CardToMove *_cardToMove) + : card(_card), position(_position), cardToMove(_cardToMove), xCoord(_card->getX()), yCoord(_card->getY()) + + { + } + bool operator<(const MoveCardStruct &other) const + { + return (yCoord == other.yCoord && + ((xCoord == other.xCoord && position < other.position) || xCoord < other.xCoord)) || + yCoord < other.yCoord; + } +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp new file mode 100644 index 000000000..1175e4b57 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -0,0 +1,598 @@ +#include "server_player.h" + +#include "../server.h" +#include "../server_abstractuserinterface.h" +#include "../server_database_interface.h" +#include "../server_room.h" +#include "server_card.h" +#include "server_cardzone.h" +#include "server_counter.h" +#include "server_game.h" +#include "server_move_card_struct.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Server_Player::Server_Player(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractPlayer(_game, _playerId, _userInfo, _judge, _userInterface) +{ +} + +Server_Player::~Server_Player() = default; + +int Server_Player::newCounterId() const +{ + int id = 0; + QMapIterator i(counters); + while (i.hasNext()) { + Server_Counter *c = i.next().value(); + if (c->getId() > id) { + id = c->getId(); + } + } + return id + 1; +} + +void Server_Player::setupZones() +{ + Server_AbstractPlayer::setupZones(); + + // This may need to be customized according to the game rules. + // ------------------------------------------------------------------ + + // Create zones + auto *deckZone = new Server_CardZone(this, ZoneNames::DECK, false, ServerInfo_Zone::HiddenZone); + addZone(deckZone); + auto *sbZone = new Server_CardZone(this, ZoneNames::SIDEBOARD, false, ServerInfo_Zone::HiddenZone); + addZone(sbZone); + addZone(new Server_CardZone(this, ZoneNames::TABLE, true, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::HAND, false, ServerInfo_Zone::PrivateZone)); + addZone(new Server_CardZone(this, ZoneNames::STACK, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::GRAVE, false, ServerInfo_Zone::PublicZone)); + addZone(new Server_CardZone(this, ZoneNames::EXILE, false, ServerInfo_Zone::PublicZone)); + + addCounter(new Server_Counter(0, "life", makeColor(255, 255, 255), 25, game->getStartingLifeTotal())); + addCounter(new Server_Counter(1, "w", makeColor(255, 255, 150), 20, 0)); + addCounter(new Server_Counter(2, "u", makeColor(150, 150, 255), 20, 0)); + addCounter(new Server_Counter(3, "b", makeColor(150, 150, 150), 20, 0)); + addCounter(new Server_Counter(4, "r", makeColor(250, 150, 150), 20, 0)); + addCounter(new Server_Counter(5, "g", makeColor(150, 255, 150), 20, 0)); + addCounter(new Server_Counter(6, "x", makeColor(255, 255, 255), 20, 0)); + addCounter(new Server_Counter(7, "storm", makeColor(255, 150, 30), 20, 0)); + + // ------------------------------------------------------------------ + + // Assign card ids and create deck from deck list + auto insertCardsIntoZone = [this](auto cards, auto *zone) { + for (auto card : cards) { + for (int k = 0; k < card->getNumber(); ++k) { + zone->insertCard(new Server_Card(card->toCardRef(), nextCardId++, 0, 0, zone), -1, 0); + } + } + }; + + insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_MAIN}), deckZone); + insertCardsIntoZone(deck->getCardNodes({DECK_ZONE_SIDE}), sbZone); + + const QList &sideboardPlan = deck->getCurrentSideboardPlan(); + for (const auto &m : sideboardPlan) { + const QString startZone = nameFromStdString(m.start_zone()); + const QString targetZone = nameFromStdString(m.target_zone()); + + Server_CardZone *start, *target; + if (startZone == DECK_ZONE_MAIN) { + start = deckZone; + } else if (startZone == DECK_ZONE_SIDE) { + start = sbZone; + } else { + continue; + } + if (targetZone == DECK_ZONE_MAIN) { + target = deckZone; + } else if (targetZone == DECK_ZONE_SIDE) { + target = sbZone; + } else { + continue; + } + + for (int j = 0; j < start->getCards().size(); ++j) { + if (start->getCards()[j]->getName() == nameFromStdString(m.card_name())) { + Server_Card *card = start->getCard(j, nullptr, true); + target->insertCard(card, -1, 0); + break; + } + } + } + + deckZone->shuffle(); +} + +void Server_Player::clearZones() +{ + Server_AbstractPlayer::clearZones(); + for (Server_Counter *counter : counters) { + delete counter; + } + counters.clear(); + + lastDrawList.clear(); +} + +void Server_Player::addCounter(Server_Counter *counter) +{ + counters.insert(counter->getId(), counter); +} + +Response::ResponseCode Server_Player::drawCards(GameEventStorage &ges, int number) +{ + Server_CardZone *deckZone = zones.value(ZoneNames::DECK); + Server_CardZone *handZone = zones.value(ZoneNames::HAND); + if (deckZone->getCards().size() < number) { + number = deckZone->getCards().size(); + } + + Event_DrawCards eventOthers; + eventOthers.set_number(number); + Event_DrawCards eventPrivate(eventOthers); + + for (int i = 0; i < number; ++i) { + Server_Card *card = deckZone->getCard(0, nullptr, true); + handZone->insertCard(card, -1, 0); + lastDrawList.append(card->getId()); + + ServerInfo_Card *cardInfo = eventPrivate.add_cards(); + cardInfo->set_id(card->getId()); + cardInfo->set_name(card->getName().toStdString()); + cardInfo->set_provider_id(card->getProviderId().toStdString()); + } + + ges.enqueueGameEvent(eventPrivate, playerId, GameEventStorageItem::SendToPrivate, playerId); + ges.enqueueGameEvent(eventOthers, playerId, GameEventStorageItem::SendToOthers); + + if (number > 0) { + revealTopCardIfNeeded(deckZone, ges); + int currentKnownCards = deckZone->getCardsBeingLookedAt(); + deckZone->setCardsBeingLookedAt(currentKnownCards - number); + } + + return Response::RespOk; +} + +void Server_Player::onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw) +{ + Server_AbstractPlayer::onCardBeingMoved(ges, cardStruct, startzone, targetzone, undoingDraw); + + Server_Card *card = cardStruct.card; + + // "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() == ZoneNames::HAND && targetzone->getName() != ZoneNames::HAND && !undoingDraw) { + int index = lastDrawList.lastIndexOf(card->getId()); + if (index != -1) { + lastDrawList.erase(lastDrawList.begin(), lastDrawList.begin() + index); + } + } +} + +Response::ResponseCode +Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + if (game->getGameStarted()) { + return Response::RespContextError; + } + + DeckList *newDeck; + if (cmd.has_deck_id()) { + try { + newDeck = game->getRoom()->getServer()->getDatabaseInterface()->getDeckFromDatabase(cmd.deck_id(), + userInfo->id()); + } catch (Response::ResponseCode &r) { + return r; + } + } else { + newDeck = new DeckList(fileFromStdString(cmd.deck())); + } + + if (!newDeck) { + return Response::RespInternalError; + } + + delete deck; + deck = newDeck; + sideboardLocked = true; + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_sideboard_locked(true); + event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString()); + ges.enqueueGameEvent(event, playerId); + + Context_DeckSelect context; + context.set_deck_hash(deck->getDeckHash().toStdString()); + context.set_sideboard_size(deck->getSideboardSize()); + if (game->getShareDecklistsOnLoad()) { + context.set_deck_list(deck->writeToString_Native().toStdString()); + } + ges.setGameEventContext(context); + + auto *re = new Response_DeckDownload; + re->set_deck(deck->writeToString_Native().toStdString()); + + rc.setResponseExtension(re); + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (readyStart) { + return Response::RespContextError; + } + if (!deck) { + return Response::RespContextError; + } + if (sideboardLocked) { + return Response::RespContextError; + } + + QList sideboardPlan; + for (int i = 0; i < cmd.move_list_size(); ++i) { + sideboardPlan.append(cmd.move_list(i)); + } + deck->setCurrentSideboardPlan(sideboardPlan); + + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetSideboardLock(const Command_SetSideboardLock &cmd, + ResponseContainer & /*rc*/, + GameEventStorage &ges) +{ + if (readyStart) { + return Response::RespContextError; + } + if (!deck) { + return Response::RespContextError; + } + if (sideboardLocked == cmd.locked()) { + return Response::RespContextError; + } + + sideboardLocked = cmd.locked(); + if (sideboardLocked) { + deck->setCurrentSideboardPlan(QList()); + } + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_sideboard_locked(sideboardLocked); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_SetSideboardLock()); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdShuffle(const Command_Shuffle &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (conceded) { + return Response::RespContextError; + } + + if (cmd.has_zone_name() && cmd.zone_name() != ZoneNames::DECK) { + return Response::RespFunctionNotAllowed; + } + + Server_CardZone *zone = zones.value(ZoneNames::DECK); + if (!zone) { + return Response::RespNameNotFound; + } + + zone->shuffle(cmd.start(), cmd.end()); + + Event_Shuffle event; + event.set_zone_name(zone->getName().toStdString()); + event.set_start(cmd.start()); + event.set_end(cmd.end()); + ges.enqueueGameEvent(event, playerId); + revealTopCardIfNeeded(zone, ges); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdMulligan(const Command_Mulligan &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_CardZone *hand = zones.value(ZoneNames::HAND); + Server_CardZone *_deck = zones.value(ZoneNames::DECK); + int number = cmd.number(); + + if (!hand->getCards().isEmpty()) { + auto cardsToMove = QList(); + for (auto &card : hand->getCards()) { + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(card->getId()); + cardsToMove.append(cardToMove); + } + moveCard(ges, hand, cardsToMove, _deck, -1, 0, false); + qDeleteAll(cardsToMove); + } + + _deck->shuffle(); + ges.enqueueGameEvent(Event_Shuffle(), playerId); + + drawCards(ges, number); + + Context_Mulligan context; + context.set_number(static_cast(hand->getCards().size())); + ges.setGameEventContext(context); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + return drawCards(ges, cmd.number()); +} + +Response::ResponseCode +Server_Player::cmdUndoDraw(const Command_UndoDraw & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + if (lastDrawList.isEmpty()) { + return Response::RespContextError; + } + + Response::ResponseCode retVal; + auto *cardToMove = new CardToMove; + cardToMove->set_card_id(lastDrawList.takeLast()); + retVal = moveCard(ges, zones.value(ZoneNames::HAND), QList() << cardToMove, + zones.value(ZoneNames::DECK), 0, 0, false, true); + delete cardToMove; + + return retVal; +} + +Response::ResponseCode +Server_Player::cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *c = counters.value(cmd.counter_id(), 0); + if (!c) { + return Response::RespNameNotFound; + } + + c->setCount(c->getCount() + cmd.delta()); + + Event_SetCounter event; + event.set_counter_id(c->getId()); + event.set_value(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + auto *c = new Server_Counter(newCounterId(), nameFromStdString(cmd.counter_name()), cmd.counter_color(), + cmd.radius(), cmd.value()); + addCounter(c); + + Event_CreateCounter event; + ServerInfo_Counter *counterInfo = event.mutable_counter_info(); + counterInfo->set_id(c->getId()); + counterInfo->set_name(c->getName().toStdString()); + counterInfo->mutable_counter_color()->CopyFrom(cmd.counter_color()); + counterInfo->set_radius(c->getRadius()); + counterInfo->set_count(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *c = counters.value(cmd.counter_id(), 0); + if (!c) { + return Response::RespNameNotFound; + } + + c->setCount(cmd.value()); + + Event_SetCounter event; + event.set_counter_id(c->getId()); + event.set_value(c->getCount()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + if (conceded) { + return Response::RespContextError; + } + + Server_Counter *counter = counters.value(cmd.counter_id(), 0); + if (!counter) { + return Response::RespNameNotFound; + } + counters.remove(cmd.counter_id()); + delete counter; + + Event_DelCounter event; + event.set_counter_id(cmd.counter_id()); + ges.enqueueGameEvent(event, playerId); + + return Response::RespOk; +} + +Response::ResponseCode +Server_Player::cmdNextTurn(const Command_NextTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (conceded && !judge) { + return Response::RespContextError; + } + + game->nextTurn(); + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdSetActivePhase(const Command_SetActivePhase &cmd, + ResponseContainer & /*rc*/, + GameEventStorage & /*ges*/) +{ + if (!game->getGameStarted()) { + return Response::RespGameNotStarted; + } + + if (!judge) { + if (conceded) { + return Response::RespContextError; + } + + if (game->getActivePlayer() != playerId) { + return Response::RespContextError; + } + } + + game->setActivePhase(cmd.phase()); + + return Response::RespOk; +} + +Response::ResponseCode Server_Player::cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) +{ + auto ret = Server_AbstractPlayer::cmdChangeZoneProperties(cmd, rc, ges); + + Server_CardZone *zone = zones.value(nameFromStdString(cmd.zone_name())); + if (!zone) { + return Response::RespNameNotFound; + } + + revealTopCardIfNeeded(zone, ges); + return ret; +} + +Response::ResponseCode +Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) +{ + + if (!judge && conceded) { + return Response::RespContextError; + } + return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges); +} + +void Server_Player::getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *recipient, + bool omniscient, + bool withUserInfo) +{ + Server_AbstractPlayer::getInfo(info, recipient, omniscient, withUserInfo); + + for (Server_Counter *counter : counters) { + counter->getInfo(info->add_counter_list()); + } +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h new file mode 100644 index 000000000..5925ed3c2 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.h @@ -0,0 +1,75 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include "server_abstract_player.h" + +class Server_Player : public Server_AbstractPlayer +{ + Q_OBJECT +private: + QMap counters; + QList lastDrawList; + +public: + Server_Player(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); + ~Server_Player() override; + const QMap &getCounters() const + { + return counters; + } + int newCounterId() const; + void addCounter(Server_Counter *counter); + + void setupZones() override; + void clearZones() override; + + Response::ResponseCode drawCards(GameEventStorage &ges, int number); + void onCardBeingMoved(GameEventStorage &ges, + const MoveCardStruct &cardStruct, + Server_CardZone *startzone, + Server_CardZone *targetzone, + bool undoingDraw) override; + + Response::ResponseCode + cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdMulligan(const Command_Mulligan &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdDrawCards(const Command_DrawCards &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdUndoDraw(const Command_UndoDraw &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdIncCounter(const Command_IncCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdCreateCounter(const Command_CreateCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetCounter(const Command_SetCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdDelCounter(const Command_DelCounter &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdNextTurn(const Command_NextTurn &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override; + Response::ResponseCode + cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override; + Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd, + ResponseContainer &rc, + GameEventStorage &ges) override; + + void getInfo(ServerInfo_Player *info, + Server_AbstractParticipant *playerWhosAsking, + bool omniscient, + bool withUserInfo) override; +}; + +#endif diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp new file mode 100644 index 000000000..bc873e386 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.cpp @@ -0,0 +1,11 @@ +#include "server_spectator.h" + +Server_Spectator::Server_Spectator(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_userInterface) + : Server_AbstractParticipant(_game, _playerId, _userInfo, _judge, _userInterface) +{ + spectator = true; +} diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h new file mode 100644 index 000000000..4d3928a63 --- /dev/null +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_spectator.h @@ -0,0 +1,17 @@ +#ifndef SPECTATOR_H +#define SPECTATOR_H + +#include "server_abstract_participant.h" + +class Server_Spectator : public Server_AbstractParticipant +{ + Q_OBJECT +public: + Server_Spectator(Server_Game *_game, + int _playerId, + const ServerInfo_User &_userInfo, + bool _judge, + Server_AbstractUserInterface *_handler); +}; + +#endif diff --git a/common/room_message_type.h b/libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h similarity index 86% rename from common/room_message_type.h rename to libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h index 7b7ad9aca..5559db965 100644 --- a/common/room_message_type.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/room_message_type.h @@ -6,9 +6,8 @@ // https://github.com/protocolbuffers/protobuf/issues/119 #undef TYPE_BOOL #endif -#include "pb/event_room_say.pb.h" - #include +#include Q_DECLARE_FLAGS(RoomMessageTypeFlags, Event_RoomSay::RoomMessageType) Q_DECLARE_OPERATORS_FOR_FLAGS(RoomMessageTypeFlags) diff --git a/common/server.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp similarity index 96% rename from common/server.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server.cpp index 4b0120ea1..a5a74c54c 100644 --- a/common/server.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.cpp @@ -19,18 +19,9 @@ ***************************************************************************/ #include "server.h" -#include "debug_pb_message.h" -#include "featureset.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_list_rooms.pb.h" -#include "pb/event_user_joined.pb.h" -#include "pb/event_user_left.pb.h" -#include "pb/isl_message.pb.h" -#include "pb/session_event.pb.h" -#include "server_counter.h" +#include "game/server_game.h" +#include "game/server_player.h" #include "server_database_interface.h" -#include "server_game.h" -#include "server_player.h" #include "server_protocolhandler.h" #include "server_remoteuserinterface.h" #include "server_room.h" @@ -38,6 +29,13 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include Server::Server(QObject *parent) : QObject(parent), nextLocalGameId(0), tcpUserCount(0), webSocketUserCount(0) { @@ -330,11 +328,11 @@ void Server::externalUserLeft(const QString &userName) continue; QMutexLocker gameLocker(&game->gameMutex); - Server_Player *player = game->getPlayers().value(userGamesIterator.value().second); - if (!player) + auto *participant = game->getParticipants().value(userGamesIterator.value().second); + if (!participant) continue; - player->disconnectClient(); + participant->disconnectClient(); } roomsLock.unlock(); @@ -481,8 +479,8 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, } QMutexLocker gameLocker(&game->gameMutex); - Server_Player *player = game->getPlayers().value(playerId); - if (!player) { + auto *participant = game->getParticipants().value(playerId); + if (!participant) { qDebug() << "externalGameCommandContainerReceived: player id=" << playerId << "not found"; throw Response::RespNotInRoom; } @@ -492,7 +490,7 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, const GameCommand &sc = cont.game_command(i); qDebug() << "[ISL]" << getSafeDebugString(sc); - Response::ResponseCode resp = player->processGameCommand(sc, responseContainer, ges); + Response::ResponseCode resp = participant->processGameCommand(sc, responseContainer, ges); if (resp != Response::RespOk) finalResponseCode = resp; @@ -500,9 +498,7 @@ void Server::externalGameCommandContainerReceived(const CommandContainer &cont, ges.sendToGame(game); if (finalResponseCode != Response::RespNothing) { - player->playerMutex.lock(); - player->getUserInterface()->sendResponseContainer(responseContainer, finalResponseCode); - player->playerMutex.unlock(); + participant->getUserInterface()->sendResponseContainer(responseContainer, finalResponseCode); } } catch (Response::ResponseCode code) { Response response; diff --git a/common/server.h b/libcockatrice_network/libcockatrice/network/server/remote/server.h similarity index 97% rename from common/server.h rename to libcockatrice_network/libcockatrice/network/server/remote/server.h index 293aeb5b2..ab57fac4e 100644 --- a/common/server.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server.h @@ -1,19 +1,15 @@ #ifndef SERVER_H #define SERVER_H -#include "pb/commands.pb.h" -#include "pb/serverinfo_ban.pb.h" -#include "pb/serverinfo_chat_message.pb.h" -#include "pb/serverinfo_user.pb.h" -#include "pb/serverinfo_warning.pb.h" #include "server_player_reference.h" -#include #include #include #include #include -#include +#include +#include +#include class Server_DatabaseInterface; class Server_Game; diff --git a/common/server_abstractuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp similarity index 89% rename from common/server_abstractuserinterface.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp index f40e64f8c..f9b61ab48 100644 --- a/common/server_abstractuserinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.cpp @@ -1,18 +1,16 @@ #include "server_abstractuserinterface.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_game_state_changed.pb.h" +#include "game/server_game.h" +#include "game/server_player.h" #include "server.h" -#include "server_game.h" -#include "server_player.h" #include "server_player_reference.h" #include "server_response_containers.h" #include "server_room.h" #include #include -#include #include +#include void Server_AbstractUserInterface::sendProtocolItemByType(ServerMessage::MessageType type, const ::google::protobuf::Message &item) @@ -35,7 +33,7 @@ void Server_AbstractUserInterface::sendProtocolItemByType(ServerMessage::Message SessionEvent *Server_AbstractUserInterface::prepareSessionEvent(const ::google::protobuf::Message &sessionEvent) { - SessionEvent *event = new SessionEvent; + auto *event = new SessionEvent; event->GetReflection() ->MutableMessage(event, sessionEvent.GetDescriptor()->FindExtensionByName("ext")) ->CopyFrom(sessionEvent); @@ -103,14 +101,14 @@ void Server_AbstractUserInterface::joinPersistentGames(ResponseContainer &rc) continue; QMutexLocker gameLocker(&game->gameMutex); - Server_Player *player = game->getPlayers().value(pr.getPlayerId()); - if (!player) + auto *participant = game->getParticipants().value(pr.getPlayerId()); + if (!participant) continue; - player->setUserInterface(this); - playerAddedToGame(game->getGameId(), room->getId(), player->getPlayerId()); + participant->setUserInterface(this); + playerAddedToGame(game->getGameId(), room->getId(), participant->getPlayerId()); - game->createGameJoinedEvent(player, rc, true); + game->createGameJoinedEvent(participant, rc, true); } server->roomsLock.unlock(); } diff --git a/common/server_abstractuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h similarity index 94% rename from common/server_abstractuserinterface.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h index 763bcc567..b11260003 100644 --- a/common/server_abstractuserinterface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_abstractuserinterface.h @@ -1,13 +1,12 @@ #ifndef SERVER_ABSTRACTUSERINTERFACE #define SERVER_ABSTRACTUSERINTERFACE -#include "pb/response.pb.h" -#include "pb/server_message.pb.h" #include "serverinfo_user_container.h" #include #include -#include +#include +#include class SessionEvent; class GameEventContainer; diff --git a/common/server_database_interface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.cpp similarity index 100% rename from common/server_database_interface.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.cpp diff --git a/common/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h similarity index 98% rename from common/server_database_interface.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h index fdbded4ba..b43dbde42 100644 --- a/common/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -83,7 +83,7 @@ public: virtual bool usernameIsValid(const QString & /*userName */, QString & /* error */) { return true; - }; + } public slots: virtual void endSession(qint64 /* sessionId */) { @@ -146,22 +146,24 @@ public: const QString & /* logMessage */, LogMessage_TargetType /* targetType */, const int /* targetId */, - const QString & /* targetName */){}; + const QString & /* targetName */) + { + } virtual bool checkUserIsBanned(Server_ProtocolHandler * /* session */, QString & /* banReason */, int & /* banSecondsRemaining */) { return false; - }; + } virtual int checkNumberOfUserAccounts(const QString & /* email */) { return 0; - }; + } virtual bool changeUserPassword(const QString & /* user */, const QString & /* password */, bool /* passwordNeedsHash */) { return false; - }; + } virtual bool changeUserPassword(const QString & /* user */, const QString & /* oldPassword */, bool /* oldPasswordNeedsHash */, @@ -169,7 +171,7 @@ public: bool /* newPasswordNeedsHash */) { return false; - }; + } }; #endif diff --git a/common/server_player_reference.h b/libcockatrice_network/libcockatrice/network/server/remote/server_player_reference.h similarity index 100% rename from common/server_player_reference.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_player_reference.h diff --git a/common/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp similarity index 91% rename from common/server_protocolhandler.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index d69ff9ba0..bfd8d113c 100644 --- a/common/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -1,32 +1,32 @@ #include "server_protocolhandler.h" -#include "debug_pb_message.h" -#include "featureset.h" -#include "get_pb_extension.h" -#include "pb/commands.pb.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_list_rooms.pb.h" -#include "pb/event_notify_user.pb.h" -#include "pb/event_room_say.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/response.pb.h" -#include "pb/response_get_games_of_user.pb.h" -#include "pb/response_get_user_info.pb.h" -#include "pb/response_join_room.pb.h" -#include "pb/response_list_users.pb.h" -#include "pb/response_login.pb.h" -#include "pb/serverinfo_user.pb.h" +#include "game/server_game.h" +#include "game/server_player.h" #include "server_database_interface.h" -#include "server_game.h" -#include "server_player.h" #include "server_room.h" -#include "trice_limits.h" #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, @@ -73,14 +73,14 @@ void Server_ProtocolHandler::prepareDestroy() continue; } game->gameMutex.lock(); - Server_Player *p = game->getPlayers().value(gameIterator.value().second); - if (!p) { + auto *participant = game->getParticipants().value(gameIterator.value().second); + if (!participant) { game->gameMutex.unlock(); room->gamesLock.unlock(); continue; } - p->disconnectClient(); + participant->disconnectClient(); game->gameMutex.unlock(); room->gamesLock.unlock(); @@ -257,8 +257,8 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const } QMutexLocker gameLocker(&game->gameMutex); - Server_Player *player = game->getPlayers().value(roomIdAndPlayerId.second); - if (!player) + auto *participant = game->getParticipants().value(roomIdAndPlayerId.second); + if (!participant) return Response::RespNotInRoom; resetIdleTimer(); @@ -289,7 +289,7 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const } } - Response::ResponseCode resp = player->processGameCommand(sc, rc, ges); + Response::ResponseCode resp = participant->processGameCommand(sc, rc, ges); if (resp != Response::RespOk) finalResponseCode = resp; @@ -473,7 +473,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd if (!missingClientFeatures.isEmpty()) { if (features.isRequiredFeaturesMissing(missingClientFeatures, server->getServerRequiredFeatureList())) { - Response_Login *re = new Response_Login; + auto *re = new Response_Login; re->set_denied_reason_str("Client upgrade required"); QMap::iterator i; for (i = missingClientFeatures.begin(); i != missingClientFeatures.end(); ++i) { @@ -491,7 +491,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd clientId, clientVersion, connectionType); switch (res) { case UserIsBanned: { - Response_Login *re = new Response_Login; + auto *re = new Response_Login; re->set_denied_reason_str(reasonStr.toStdString()); if (banSecondsLeft != 0) re->set_denied_end_time(QDateTime::currentDateTime().addSecs(banSecondsLeft).toSecsSinceEpoch()); @@ -503,7 +503,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd case WouldOverwriteOldSession: return Response::RespWouldOverwriteOldSession; case UsernameInvalid: { - Response_Login *re = new Response_Login; + auto *re = new Response_Login; re->set_denied_reason_str(reasonStr.toStdString()); rc.setResponseExtension(re); return Response::RespUsernameInvalid; @@ -534,7 +534,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd event.set_message(server->getLoginMessage().toStdString()); rc.enqueuePostResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); - Response_Login *re = new Response_Login; + auto *re = new Response_Login; re->mutable_user_info()->CopyFrom(copyUserInfo(true)); if (authState == PasswordRight) { @@ -602,10 +602,21 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetGamesOfUser(const Command_G if (authState == NotLoggedIn) return Response::RespLoginNeeded; + // Do not show games to someone on the ignore list of that user, except for mods + QString target_user = nameFromStdString(cmd.user_name()); + Server_AbstractUserInterface *userInterface = server->findUser(target_user); + if (!userInterface) { + return Response::RespNameNotFound; + } + if (!(userInfo->user_level() & (ServerInfo_User::IsModerator | ServerInfo_User::IsAdmin)) && + databaseInterface->isInIgnoreList(target_user, QString::fromStdString(userInfo->name()))) { + return Response::RespInIgnoreList; + } + // We don't need to check whether the user is logged in; persistent games should also work. // The client needs to deal with an empty result list. - Response_GetGamesOfUser *re = new Response_GetGamesOfUser; + auto *re = new Response_GetGamesOfUser; server->roomsLock.lockForRead(); QMapIterator roomIterator(server->getRooms()); while (roomIterator.hasNext()) { @@ -629,7 +640,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetUserInfo(const Command_GetU return Response::RespLoginNeeded; QString userName = nameFromStdString(cmd.user_name()); - Response_GetUserInfo *re = new Response_GetUserInfo; + auto *re = new Response_GetUserInfo; if (userName.isEmpty()) re->mutable_user_info()->CopyFrom(*userInfo); else { @@ -702,7 +713,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoo joinMessageEvent.set_message_type(Event_RoomSay::Welcome); rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(joinMessageEvent)); - Response_JoinRoom *re = new Response_JoinRoom; + auto *re = new Response_JoinRoom; room->getInfo(*re->mutable_room_info(), true); rc.setResponseExtension(re); @@ -714,7 +725,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdListUsers(const Command_ListUs if (authState == NotLoggedIn) return Response::RespLoginNeeded; - Response_ListUsers *re = new Response_ListUsers; + auto *re = new Response_ListUsers; server->clientsLock.lockForRead(); QMapIterator userIterator = server->getUsers(); while (userIterator.hasNext()) @@ -827,10 +838,10 @@ Server_ProtocolHandler::cmdCreateGame(const Command_CreateGame &cmd, Server_Room // When server doesn't permit registered users to exist, do not honor only-reg setting bool onlyRegisteredUsers = cmd.only_registered() && (server->permitUnregisteredUsers()); - Server_Game *game = new Server_Game( - copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), cmd.max_players(), gameTypes, - cmd.only_buddies(), onlyRegisteredUsers, cmd.spectators_allowed(), cmd.spectators_need_password(), - cmd.spectators_can_talk(), cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); + auto *game = new Server_Game(copyUserInfo(false), gameId, description, QString::fromStdString(cmd.password()), + cmd.max_players(), gameTypes, cmd.only_buddies(), onlyRegisteredUsers, + cmd.spectators_allowed(), cmd.spectators_need_password(), cmd.spectators_can_talk(), + cmd.spectators_see_everything(), startingLifeTotal, shareDecklistsOnLoad, room); game->addPlayer(this, rc, asSpectator, asJudge, false); room->addGame(game); diff --git a/common/server_protocolhandler.h b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h similarity index 97% rename from common/server_protocolhandler.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h index b80e08c7e..0d05b91c8 100644 --- a/common/server_protocolhandler.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.h @@ -1,13 +1,12 @@ #ifndef SERVER_PROTOCOLHANDLER_H #define SERVER_PROTOCOLHANDLER_H -#include "pb/response.pb.h" -#include "pb/server_message.pb.h" #include "server.h" #include "server_abstractuserinterface.h" #include -#include +#include +#include class Features; class Server_DatabaseInterface; diff --git a/common/server_remoteuserinterface.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp similarity index 92% rename from common/server_remoteuserinterface.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp index ec9b5d88a..54af5a265 100644 --- a/common/server_remoteuserinterface.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.cpp @@ -1,8 +1,9 @@ #include "server_remoteuserinterface.h" -#include "pb/serverinfo_user.pb.h" #include "server.h" +#include + void Server_RemoteUserInterface::sendProtocolItem(const Response &item) { server->sendIsl_Response(item, userInfo->server_id(), userInfo->session_id()); diff --git a/common/server_remoteuserinterface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.h similarity index 100% rename from common/server_remoteuserinterface.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_remoteuserinterface.h diff --git a/common/server_response_containers.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp similarity index 99% rename from common/server_response_containers.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp index 03ce4d853..9b07bdb91 100644 --- a/common/server_response_containers.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.cpp @@ -1,6 +1,6 @@ #include "server_response_containers.h" -#include "server_game.h" +#include "game/server_game.h" #include diff --git a/common/server_response_containers.h b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h similarity index 79% rename from common/server_response_containers.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h index 31e00c54e..118b32d38 100644 --- a/common/server_response_containers.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_response_containers.h @@ -1,10 +1,9 @@ #ifndef SERVER_RESPONSE_CONTAINERS_H #define SERVER_RESPONSE_CONTAINERS_H -#include "pb/server_message.pb.h" - #include #include +#include namespace google { @@ -32,11 +31,11 @@ public: GameEventStorageItem(const ::google::protobuf::Message &_event, int _playerId, EventRecipients _recipients); ~GameEventStorageItem(); - const GameEvent &getGameEvent() const + [[nodiscard]] const GameEvent &getGameEvent() const { return *event; } - EventRecipients getRecipients() const + [[nodiscard]] EventRecipients getRecipients() const { return recipients; } @@ -57,15 +56,15 @@ public: ~GameEventStorage(); void setGameEventContext(const ::google::protobuf::Message &_gameEventContext); - ::google::protobuf::Message *getGameEventContext() const + [[nodiscard]] ::google::protobuf::Message *getGameEventContext() const { return gameEventContext; } - const QList &getGameEventList() const + [[nodiscard]] const QList &getGameEventList() const { return gameEventList; } - int getPrivatePlayerId() const + [[nodiscard]] int getPrivatePlayerId() const { return privatePlayerId; } @@ -97,7 +96,7 @@ public: ResponseContainer(int _cmdId); ~ResponseContainer(); - int getCmdId() const + [[nodiscard]] int getCmdId() const { return cmdId; } @@ -105,7 +104,7 @@ public: { responseExtension = _responseExtension; } - ::google::protobuf::Message *getResponseExtension() const + [[nodiscard]] ::google::protobuf::Message *getResponseExtension() const { return responseExtension; } @@ -117,11 +116,13 @@ public: { postResponseQueue.append(qMakePair(type, item)); } - const QList> &getPreResponseQueue() const + [[nodiscard]] const QList> & + getPreResponseQueue() const { return preResponseQueue; } - const QList> &getPostResponseQueue() const + [[nodiscard]] const QList> & + getPostResponseQueue() const { return postResponseQueue; } diff --git a/common/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp similarity index 95% rename from common/server_room.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 654666edf..bfa8912b1 100644 --- a/common/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -1,21 +1,21 @@ #include "server_room.h" -#include "pb/commands.pb.h" -#include "pb/event_join_room.pb.h" -#include "pb/event_leave_room.pb.h" -#include "pb/event_list_games.pb.h" -#include "pb/event_remove_messages.pb.h" -#include "pb/event_room_say.pb.h" -#include "pb/room_commands.pb.h" -#include "pb/serverinfo_chat_message.pb.h" -#include "pb/serverinfo_room.pb.h" -#include "server_game.h" +#include "game/server_game.h" #include "server_protocolhandler.h" -#include "trice_limits.h" #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include Server_Room::Server_Room(int _id, int _chatHistorySize, @@ -128,7 +128,7 @@ Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, RoomEvent *Server_Room::prepareRoomEvent(const ::google::protobuf::Message &roomEvent) { - RoomEvent *event = new RoomEvent; + auto *event = new RoomEvent; event->set_room_id(id); event->GetReflection() ->MutableMessage(event, roomEvent.GetDescriptor()->FindExtensionByName("ext")) diff --git a/common/server_room.h b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h similarity index 97% rename from common/server_room.h rename to libcockatrice_network/libcockatrice/network/server/remote/server_room.h index 7c804a461..3d9988f20 100644 --- a/common/server_room.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.h @@ -1,16 +1,14 @@ #ifndef SERVER_ROOM_H #define SERVER_ROOM_H -#include "pb/response.pb.h" -#include "pb/serverinfo_chat_message.pb.h" #include "serverinfo_user_container.h" -#include #include #include #include #include -#include +#include +#include class Server_DatabaseInterface; class Server_ProtocolHandler; diff --git a/common/serverinfo_user_container.cpp b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp similarity index 96% rename from common/serverinfo_user_container.cpp rename to libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp index 597ebd01a..77ff38906 100644 --- a/common/serverinfo_user_container.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.cpp @@ -1,6 +1,6 @@ #include "serverinfo_user_container.h" -#include "pb/serverinfo_user.pb.h" +#include ServerInfo_User_Container::ServerInfo_User_Container(ServerInfo_User *_userInfo) : userInfo(_userInfo) { diff --git a/common/serverinfo_user_container.h b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h similarity index 81% rename from common/serverinfo_user_container.h rename to libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h index 4a29bce2d..a959f4535 100644 --- a/common/serverinfo_user_container.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/serverinfo_user_container.h @@ -14,14 +14,15 @@ public: ServerInfo_User_Container(const ServerInfo_User_Container &other); ServerInfo_User_Container &operator=(const ServerInfo_User_Container &other) = default; virtual ~ServerInfo_User_Container(); - ServerInfo_User *getUserInfo() const + [[nodiscard]] ServerInfo_User *getUserInfo() const { return userInfo; } void setUserInfo(const ServerInfo_User &_userInfo); ServerInfo_User & copyUserInfo(ServerInfo_User &result, bool complete, bool internalInfo = false, bool sessionInfo = false) const; - ServerInfo_User copyUserInfo(bool complete, bool internalInfo = false, bool sessionInfo = false) const; + [[nodiscard]] ServerInfo_User + copyUserInfo(bool complete, bool internalInfo = false, bool sessionInfo = false) const; }; #endif diff --git a/common/user_level.h b/libcockatrice_network/libcockatrice/network/server/remote/user_level.h similarity index 85% rename from common/user_level.h rename to libcockatrice_network/libcockatrice/network/server/remote/user_level.h index 5d720c0aa..9b7a0ca88 100644 --- a/common/user_level.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/user_level.h @@ -6,9 +6,8 @@ // https://github.com/protocolbuffers/protobuf/issues/119 #undef TYPE_BOOL #endif -#include "pb/serverinfo_user.pb.h" - #include +#include Q_DECLARE_FLAGS(UserLevelFlags, ServerInfo_User::UserLevelFlag) Q_DECLARE_OPERATORS_FOR_FLAGS(UserLevelFlags) diff --git a/libcockatrice_protocol/CMakeLists.txt b/libcockatrice_protocol/CMakeLists.txt new file mode 100644 index 000000000..7dc0a0360 --- /dev/null +++ b/libcockatrice_protocol/CMakeLists.txt @@ -0,0 +1,26 @@ +# Top-level wrapper for the protobuf library + +add_subdirectory(libcockatrice/protocol/pb) + +add_library(libcockatrice_protocol STATIC) + +set(SOURCES libcockatrice/protocol/debug_pb_message.cpp libcockatrice/protocol/featureset.cpp + libcockatrice/protocol/get_pb_extension.cpp libcockatrice/protocol/pending_command.cpp +) + +set(HEADERS libcockatrice/protocol/debug_pb_message.h libcockatrice/protocol/featureset.h + libcockatrice/protocol/get_pb_extension.h libcockatrice/protocol/pending_command.h +) + +target_sources(libcockatrice_protocol PRIVATE ${SOURCES} ${HEADERS}) + +add_dependencies(libcockatrice_protocol libcockatrice_protocol_pb) + +# Link the actual generated protobuf library +target_link_libraries(libcockatrice_protocol PUBLIC ${QT_CORE_MODULE} libcockatrice_protocol_pb libcockatrice_utility) + +# Expose include paths +target_include_directories( + libcockatrice_protocol PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} # points to the generated headers +) diff --git a/common/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp similarity index 93% rename from common/debug_pb_message.cpp rename to libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp index dda672689..718487c18 100644 --- a/common/debug_pb_message.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -1,12 +1,11 @@ #include "debug_pb_message.h" -#include "trice_limits.h" - #include #include #include #include #include +#include // FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it #if GOOGLE_PROTOBUF_VERSION > 3004000 @@ -102,6 +101,10 @@ QString getSafeDebugString(const ::google::protobuf::Message &message) #endif // GOOGLE_PROTOBUF_VERSION > 3004000 std::string debug_string; - printer.PrintToString(message, &debug_string); - return QString::number(size) + " bytes " + QString::fromStdString(debug_string); + bool ok = printer.PrintToString(message, &debug_string); + if (ok) { + return QString::number(size) + " bytes " + QString::fromStdString(debug_string); + } else { + return "[could not convert message to string]"; + } } diff --git a/common/debug_pb_message.h b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.h similarity index 100% rename from common/debug_pb_message.h rename to libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.h diff --git a/common/featureset.cpp b/libcockatrice_protocol/libcockatrice/protocol/featureset.cpp similarity index 99% rename from common/featureset.cpp rename to libcockatrice_protocol/libcockatrice/protocol/featureset.cpp index d9f253ab9..1b08c4040 100644 --- a/common/featureset.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/featureset.cpp @@ -1,6 +1,5 @@ #include "featureset.h" -#include #include FeatureSet::FeatureSet() diff --git a/common/featureset.h b/libcockatrice_protocol/libcockatrice/protocol/featureset.h similarity index 94% rename from common/featureset.h rename to libcockatrice_protocol/libcockatrice/protocol/featureset.h index da495c4b7..32625d5f8 100644 --- a/common/featureset.h +++ b/libcockatrice_protocol/libcockatrice/protocol/featureset.h @@ -2,10 +2,10 @@ #define FEATURESET_H #include -#include +#include #include -class FeatureSet +class FeatureSet : public QObject { public: FeatureSet(); diff --git a/common/get_pb_extension.cpp b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.cpp similarity index 100% rename from common/get_pb_extension.cpp rename to libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.cpp diff --git a/common/get_pb_extension.h b/libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.h similarity index 100% rename from common/get_pb_extension.h rename to libcockatrice_protocol/libcockatrice/protocol/get_pb_extension.h diff --git a/common/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt similarity index 84% rename from common/pb/CMakeLists.txt rename to libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index e8af9ef46..212ab69dd 100644 --- a/common/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -12,8 +12,8 @@ set(PROTO_FILES command_create_arrow.proto command_create_counter.proto command_create_token.proto - command_deck_del_dir.proto command_deck_del.proto + command_deck_del_dir.proto command_deck_download.proto command_deck_list.proto command_deck_new_dir.proto @@ -34,9 +34,11 @@ set(PROTO_FILES command_next_turn.proto command_ready_start.proto command_replay_delete_match.proto - command_replay_list.proto command_replay_download.proto + command_replay_get_code.proto + command_replay_list.proto command_replay_modify_match.proto + command_replay_submit_code.proto command_reveal_cards.proto command_reverse_turn.proto command_roll_die.proto @@ -44,11 +46,11 @@ set(PROTO_FILES command_set_card_attr.proto command_set_card_counter.proto command_set_counter.proto - command_set_sideboard_plan.proto command_set_sideboard_lock.proto + command_set_sideboard_plan.proto command_shuffle.proto - commands.proto command_undo_draw.proto + commands.proto context_concede.proto context_connection_state_changed.proto context_deck_select.proto @@ -76,6 +78,7 @@ set(PROTO_FILES event_game_joined.proto event_game_say.proto event_game_state_changed.proto + event_game_state_changed.proto event_join.proto event_join_room.proto event_kicked.proto @@ -84,14 +87,15 @@ set(PROTO_FILES event_list_games.proto event_list_rooms.proto event_move_card.proto + event_notify_user.proto event_player_properties_changed.proto event_remove_from_list.proto + event_remove_messages.proto event_replay_added.proto event_reveal_cards.proto event_reverse_turn.proto event_roll_die.proto event_room_say.proto - event_remove_messages.proto event_server_complete_list.proto event_server_identification.proto event_server_message.proto @@ -105,15 +109,15 @@ set(PROTO_FILES event_user_joined.proto event_user_left.proto event_user_message.proto - event_notify_user.proto game_commands.proto + game_event.proto game_event_container.proto game_event_context.proto - game_event.proto game_replay.proto isl_message.proto moderator_commands.proto move_card_to_zone.proto + response.proto response_activate.proto response_adjust_mod.proto response_ban_history.proto @@ -122,6 +126,7 @@ set(PROTO_FILES response_deck_upload.proto response_dump_zone.proto response_forgotpasswordrequest.proto + response_get_admin_notes.proto response_get_games_of_user.proto response_get_user_info.proto response_join_room.proto @@ -130,49 +135,55 @@ set(PROTO_FILES response_password_salt.proto response_register.proto response_replay_download.proto + response_replay_get_code.proto response_replay_list.proto response_viewlog_history.proto response_warn_history.proto response_warn_list.proto - response_get_admin_notes.proto - response.proto room_commands.proto room_event.proto + server_message.proto serverinfo_arrow.proto serverinfo_ban.proto - serverinfo_cardcounter.proto serverinfo_card.proto + serverinfo_cardcounter.proto serverinfo_chat_message.proto serverinfo_counter.proto serverinfo_deckstorage.proto serverinfo_game.proto serverinfo_gametype.proto + serverinfo_player.proto serverinfo_playerping.proto serverinfo_playerproperties.proto - serverinfo_player.proto serverinfo_replay.proto serverinfo_replay_match.proto serverinfo_room.proto serverinfo_user.proto serverinfo_warning.proto serverinfo_zone.proto - server_message.proto session_commands.proto session_event.proto ) +if(MSVC) + set(unused_warning /wd4100) +else() + set(unused_warning -Wno-unused-parameter) +endif() + if(${Protobuf_VERSION} VERSION_LESS "3.21.0.0") message(STATUS "Using Protobuf Legacy Mode") include_directories(${PROTOBUF_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES}) - add_library(cockatrice_protocol ${PROTO_SRCS} ${PROTO_HDRS}) - set(cockatrice_protocol_LIBS ${PROTOBUF_LIBRARIES}) + add_library(libcockatrice_protocol_pb ${PROTO_SRCS} ${PROTO_HDRS}) + target_compile_options(libcockatrice_protocol_pb PRIVATE ${unused_warning}) + set(libcockatrice_protocol_pb_LIBS ${PROTOBUF_LIBRARIES}) if(UNIX) - set(cockatrice_protocol_LIBS ${cockatrice_protocol_LIBS} -lpthread) + set(libcockatrice_protocol_pb_LIBS ${libcockatrice_protocol_pb_LIBS} -lpthread) endif(UNIX) - target_link_libraries(cockatrice_protocol ${cockatrice_protocol_LIBS}) + target_link_libraries(libcockatrice_protocol_pb ${libcockatrice_protocol_pb_LIBS}) # ubuntu uses an outdated package for protobuf, 3.1.0 is required if(${Protobuf_VERSION} VERSION_LESS "3.1.0") @@ -184,12 +195,11 @@ if(${Protobuf_VERSION} VERSION_LESS "3.21.0.0") ) endif() else() - add_library(cockatrice_protocol ${PROTO_FILES}) - target_link_libraries(cockatrice_protocol PUBLIC protobuf::libprotobuf) + add_library(libcockatrice_protocol_pb ${PROTO_FILES}) + target_compile_options(libcockatrice_protocol_pb PRIVATE ${unused_warning}) + target_link_libraries(libcockatrice_protocol_pb PUBLIC protobuf::libprotobuf) set(PROTO_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") - target_include_directories(cockatrice_protocol PUBLIC "${PROTOBUF_INCLUDE_DIRS}") + target_include_directories(libcockatrice_protocol_pb PUBLIC "${PROTOBUF_INCLUDE_DIRS}") - protobuf_generate( - TARGET cockatrice_protocol IMPORT_DIRS "${CMAKE_CURRENT_LIST_DIR}" PROTOC_OUT_DIR "${PROTO_BINARY_DIR}" - ) + protobuf_generate(TARGET libcockatrice_protocol_pb IMPORT_DIRS "." PROTOC_OUT_DIR "${PROTO_BINARY_DIR}") endif() diff --git a/common/pb/admin_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto similarity index 100% rename from common/pb/admin_commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/admin_commands.proto diff --git a/common/pb/card_attributes.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/card_attributes.proto similarity index 100% rename from common/pb/card_attributes.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/card_attributes.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto new file mode 100644 index 000000000..3daae6ce2 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/color.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +// Container for a 4 component color code +message color { + // the red component of the color, limited to 256 values + optional uint32 r = 1; + + // the green component of the color, limited to 256 values + optional uint32 g = 2; + + // the blue component of the color, limited to 256 values + optional uint32 b = 3; + + // the opacity component of the color, limited to 256 values + optional uint32 a = 4; +} diff --git a/common/pb/command_attach_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_attach_card.proto similarity index 100% rename from common/pb/command_attach_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_attach_card.proto diff --git a/common/pb/command_change_zone_properties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_change_zone_properties.proto similarity index 100% rename from common/pb/command_change_zone_properties.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_change_zone_properties.proto diff --git a/common/pb/command_concede.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_concede.proto similarity index 100% rename from common/pb/command_concede.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_concede.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto new file mode 100644 index 000000000..cb3871b60 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_arrow.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; +import "game_commands.proto"; +import "color.proto"; + +// Command to draw an arrow from cards to either other cards or a player +message Command_CreateArrow { + extend GameCommand { + optional Command_CreateArrow ext = 1011; + } + // the player that has the card the arrow is drawn from + optional sint32 start_player_id = 1 [default = -1]; + + // the zone that the card the arrow is drawn from is in + optional string start_zone = 2; + + // the id of the card that the arrow is drawn from + optional sint32 start_card_id = 3 [default = -1]; + + // the player that has the card the arrow is drawn to, or that the arrow is drawn to if not a card + optional sint32 target_player_id = 4 [default = -1]; + + // the zone that the card the arrow is drawn to is in, the player will be targeted if this is absent + optional string target_zone = 5; + + // the id of the card that the arrow is drawn to, the player will be targeted if this is absent + optional sint32 target_card_id = 6 [default = -1]; + + // the color of the arrow + optional color arrow_color = 7; + + // the phase that this arrow is deleted in, arrows are deleted when this or a later phase is activated and also + // when the phase moves back before the current phase or the turn is passed, when not set the arrow is deleted + // immediately when the phase is changed + optional sint32 delete_in_phase = 8; +} diff --git a/common/pb/command_create_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_counter.proto similarity index 100% rename from common/pb/command_create_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_create_counter.proto diff --git a/common/pb/command_create_token.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_create_token.proto similarity index 100% rename from common/pb/command_create_token.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_create_token.proto diff --git a/common/pb/command_deck_del.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del.proto similarity index 100% rename from common/pb/command_deck_del.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del.proto diff --git a/common/pb/command_deck_del_dir.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del_dir.proto similarity index 100% rename from common/pb/command_deck_del_dir.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_del_dir.proto diff --git a/common/pb/command_deck_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download.proto similarity index 100% rename from common/pb/command_deck_download.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_download.proto diff --git a/common/pb/command_deck_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list.proto similarity index 100% rename from common/pb/command_deck_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_list.proto diff --git a/common/pb/command_deck_new_dir.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_new_dir.proto similarity index 100% rename from common/pb/command_deck_new_dir.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_new_dir.proto diff --git a/common/pb/command_deck_select.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_select.proto similarity index 100% rename from common/pb/command_deck_select.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_select.proto diff --git a/common/pb/command_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto similarity index 100% rename from common/pb/command_deck_upload.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_upload.proto diff --git a/common/pb/command_del_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_del_counter.proto similarity index 100% rename from common/pb/command_del_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_del_counter.proto diff --git a/common/pb/command_delete_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_arrow.proto similarity index 100% rename from common/pb/command_delete_arrow.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_delete_arrow.proto diff --git a/common/pb/command_draw_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_draw_cards.proto similarity index 100% rename from common/pb/command_draw_cards.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_draw_cards.proto diff --git a/common/pb/command_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_dump_zone.proto similarity index 100% rename from common/pb/command_dump_zone.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_dump_zone.proto diff --git a/common/pb/command_flip_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_flip_card.proto similarity index 100% rename from common/pb/command_flip_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_flip_card.proto diff --git a/common/pb/command_game_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_game_say.proto similarity index 100% rename from common/pb/command_game_say.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_game_say.proto diff --git a/common/pb/command_inc_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_card_counter.proto similarity index 100% rename from common/pb/command_inc_card_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_card_counter.proto diff --git a/common/pb/command_inc_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_counter.proto similarity index 100% rename from common/pb/command_inc_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_inc_counter.proto diff --git a/common/pb/command_kick_from_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_kick_from_game.proto similarity index 100% rename from common/pb/command_kick_from_game.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_kick_from_game.proto diff --git a/common/pb/command_leave_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_leave_game.proto similarity index 100% rename from common/pb/command_leave_game.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_leave_game.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto new file mode 100644 index 000000000..a5b96da2e --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_move_card.proto @@ -0,0 +1,55 @@ +syntax = "proto2"; +import "game_commands.proto"; + +// Container describing a single card to move +message CardToMove { + // Id of the card in its current zone + optional sint32 card_id = 1 [default = -1]; + + // If true, places the card face down, hiding its name. + // If false, forcibly turns the card face up. + // If not set, defers the resulting face down state to the server. + optional bool face_down = 2; + + // When moving add this value to the power/toughness field of the card + optional string pt = 3; + + // When moving sets the card to be tapped + optional bool tapped = 4; +} + +// Container of multiple cards to move +message ListOfCardsToMove { + repeated CardToMove card = 1; +} + +// Command to move an amount of cards from one zone to another index/coordinate or another zone +message Command_MoveCard { + extend GameCommand { + optional Command_MoveCard ext = 1027; + } + + // The player the zone the cards are in belongs to + optional sint32 start_player_id = 1 [default = -1]; + + // The zone the cards start in + optional string start_zone = 2; + + // List of the cards and their new properties + optional ListOfCardsToMove cards_to_move = 3; + + // The player the zone the cards will be moved to belongs to + optional sint32 target_player_id = 4 [default = -1]; + + // The zone the cards will be moved to + optional string target_zone = 5; + + // New x coordinate of the first card in the list + optional sint32 x = 6 [default = -1]; + + // New y coordinate of the first card in the list + optional sint32 y = 7 [default = -1]; + + // Inverts the x coordinate to apply from the end of the target zone instead of the start + optional bool is_reversed = 8 [default = false]; +} diff --git a/common/pb/command_mulligan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_mulligan.proto similarity index 100% rename from common/pb/command_mulligan.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_mulligan.proto diff --git a/common/pb/command_next_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_next_turn.proto similarity index 100% rename from common/pb/command_next_turn.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_next_turn.proto diff --git a/common/pb/command_ready_start.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_ready_start.proto similarity index 100% rename from common/pb/command_ready_start.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_ready_start.proto diff --git a/common/pb/command_replay_delete_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_delete_match.proto similarity index 100% rename from common/pb/command_replay_delete_match.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_delete_match.proto diff --git a/common/pb/command_replay_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download.proto similarity index 100% rename from common/pb/command_replay_download.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_download.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto new file mode 100644 index 000000000..e4573153c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_get_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplayGetCode { + extend SessionCommand { + optional Command_ReplayGetCode ext = 1104; + } + optional sint32 game_id = 1 [default = -1]; +} diff --git a/common/pb/command_replay_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_list.proto similarity index 100% rename from common/pb/command_replay_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_list.proto diff --git a/common/pb/command_replay_modify_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_modify_match.proto similarity index 100% rename from common/pb/command_replay_modify_match.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_modify_match.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto new file mode 100644 index 000000000..73c5d0ba0 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_replay_submit_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_ReplaySubmitCode { + extend SessionCommand { + optional Command_ReplaySubmitCode ext = 1105; + } + optional string replay_code = 1; +} diff --git a/common/pb/command_reveal_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto similarity index 100% rename from common/pb/command_reveal_cards.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_reveal_cards.proto diff --git a/common/pb/command_reverse_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_reverse_turn.proto similarity index 100% rename from common/pb/command_reverse_turn.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_reverse_turn.proto diff --git a/common/pb/command_roll_die.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_roll_die.proto similarity index 100% rename from common/pb/command_roll_die.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_roll_die.proto diff --git a/common/pb/command_set_active_phase.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_active_phase.proto similarity index 100% rename from common/pb/command_set_active_phase.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_active_phase.proto diff --git a/common/pb/command_set_card_attr.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_attr.proto similarity index 100% rename from common/pb/command_set_card_attr.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_attr.proto diff --git a/common/pb/command_set_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_counter.proto similarity index 100% rename from common/pb/command_set_card_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_card_counter.proto diff --git a/common/pb/command_set_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter.proto similarity index 100% rename from common/pb/command_set_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_counter.proto diff --git a/common/pb/command_set_sideboard_lock.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_lock.proto similarity index 100% rename from common/pb/command_set_sideboard_lock.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_lock.proto diff --git a/common/pb/command_set_sideboard_plan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_plan.proto similarity index 100% rename from common/pb/command_set_sideboard_plan.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_set_sideboard_plan.proto diff --git a/common/pb/command_shuffle.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_shuffle.proto similarity index 100% rename from common/pb/command_shuffle.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_shuffle.proto diff --git a/common/pb/command_undo_draw.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_undo_draw.proto similarity index 100% rename from common/pb/command_undo_draw.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/command_undo_draw.proto diff --git a/common/pb/commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto similarity index 100% rename from common/pb/commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/commands.proto diff --git a/common/pb/context_concede.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_concede.proto similarity index 100% rename from common/pb/context_concede.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_concede.proto diff --git a/common/pb/context_connection_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_connection_state_changed.proto similarity index 100% rename from common/pb/context_connection_state_changed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_connection_state_changed.proto diff --git a/common/pb/context_deck_select.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_deck_select.proto similarity index 100% rename from common/pb/context_deck_select.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_deck_select.proto diff --git a/common/pb/context_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_move_card.proto similarity index 100% rename from common/pb/context_move_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_move_card.proto diff --git a/common/pb/context_mulligan.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_mulligan.proto similarity index 100% rename from common/pb/context_mulligan.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_mulligan.proto diff --git a/common/pb/context_ping_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ping_changed.proto similarity index 100% rename from common/pb/context_ping_changed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_ping_changed.proto diff --git a/common/pb/context_ready_start.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_ready_start.proto similarity index 100% rename from common/pb/context_ready_start.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_ready_start.proto diff --git a/common/pb/context_set_sideboard_lock.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_set_sideboard_lock.proto similarity index 100% rename from common/pb/context_set_sideboard_lock.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_set_sideboard_lock.proto diff --git a/common/pb/context_undo_draw.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/context_undo_draw.proto similarity index 100% rename from common/pb/context_undo_draw.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/context_undo_draw.proto diff --git a/common/pb/event_add_to_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_add_to_list.proto similarity index 100% rename from common/pb/event_add_to_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_add_to_list.proto diff --git a/common/pb/event_attach_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_attach_card.proto similarity index 100% rename from common/pb/event_attach_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_attach_card.proto diff --git a/common/pb/event_change_zone_properties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_change_zone_properties.proto similarity index 100% rename from common/pb/event_change_zone_properties.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_change_zone_properties.proto diff --git a/common/pb/event_connection_closed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_connection_closed.proto similarity index 100% rename from common/pb/event_connection_closed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_connection_closed.proto diff --git a/common/pb/event_create_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_arrow.proto similarity index 100% rename from common/pb/event_create_arrow.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_create_arrow.proto diff --git a/common/pb/event_create_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_counter.proto similarity index 100% rename from common/pb/event_create_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_create_counter.proto diff --git a/common/pb/event_create_token.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_create_token.proto similarity index 100% rename from common/pb/event_create_token.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_create_token.proto diff --git a/common/pb/event_del_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_del_counter.proto similarity index 100% rename from common/pb/event_del_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_del_counter.proto diff --git a/common/pb/event_delete_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_arrow.proto similarity index 100% rename from common/pb/event_delete_arrow.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_delete_arrow.proto diff --git a/common/pb/event_destroy_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_destroy_card.proto similarity index 100% rename from common/pb/event_destroy_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_destroy_card.proto diff --git a/common/pb/event_draw_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_draw_cards.proto similarity index 100% rename from common/pb/event_draw_cards.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_draw_cards.proto diff --git a/common/pb/event_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_dump_zone.proto similarity index 100% rename from common/pb/event_dump_zone.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_dump_zone.proto diff --git a/common/pb/event_flip_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_flip_card.proto similarity index 100% rename from common/pb/event_flip_card.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_flip_card.proto diff --git a/common/pb/event_game_closed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_closed.proto similarity index 100% rename from common/pb/event_game_closed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_game_closed.proto diff --git a/common/pb/event_game_host_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_host_changed.proto similarity index 100% rename from common/pb/event_game_host_changed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_game_host_changed.proto diff --git a/common/pb/event_game_joined.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_joined.proto similarity index 100% rename from common/pb/event_game_joined.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_game_joined.proto diff --git a/common/pb/event_game_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_say.proto similarity index 100% rename from common/pb/event_game_say.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_game_say.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto new file mode 100644 index 000000000..5c8aee3fe --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_game_state_changed.proto @@ -0,0 +1,27 @@ +syntax = "proto2"; +import "game_event.proto"; +import "serverinfo_player.proto"; + +// Signals that the game state has changed. +// If a field is present in this message, it will overwrite the client's game state. +// Also used to provide the entire game state when joining a game. +message Event_GameStateChanged { + extend GameEvent { + optional Event_GameStateChanged ext = 1005; + } + + // the list of players. Players contain their zones which contain all cards in the game + repeated ServerInfo_Player player_list = 1; + + // if the game has started + optional bool game_started = 2; + + // the player who is currently holding the turn + optional sint32 active_player_id = 3; + + // the current phase + optional sint32 active_phase = 4; + + // the amount of seconds since the game started + optional uint32 seconds_elapsed = 5; +} diff --git a/common/pb/event_join.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join.proto similarity index 100% rename from common/pb/event_join.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_join.proto diff --git a/common/pb/event_join_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_join_room.proto similarity index 100% rename from common/pb/event_join_room.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_join_room.proto diff --git a/common/pb/event_kicked.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_kicked.proto similarity index 100% rename from common/pb/event_kicked.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_kicked.proto diff --git a/common/pb/event_leave.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave.proto similarity index 100% rename from common/pb/event_leave.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_leave.proto diff --git a/common/pb/event_leave_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_leave_room.proto similarity index 100% rename from common/pb/event_leave_room.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_leave_room.proto diff --git a/common/pb/event_list_games.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_games.proto similarity index 100% rename from common/pb/event_list_games.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_list_games.proto diff --git a/common/pb/event_list_rooms.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_list_rooms.proto similarity index 100% rename from common/pb/event_list_rooms.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_list_rooms.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto new file mode 100644 index 000000000..e8e60066a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/event_move_card.proto @@ -0,0 +1,48 @@ +syntax = "proto2"; +import "game_event.proto"; + +// Sent by the server to signal a single card was moved to update the client state +message Event_MoveCard { + extend GameEvent { + optional Event_MoveCard ext = 2009; + } + + // The card id in the original zone + optional sint32 card_id = 1 [default = -1]; + + // The name of the card in case it was not known yet + optional string card_name = 2; + + // The player whose zone the card started in + optional sint32 start_player_id = 3 [default = -1]; + + // The original zone + optional string start_zone = 4; + + // The original position that the card was at. + // In zones without y coordinate, this corresponds with the previous x coordinate of the card. + // In zones with y coordinate, this value is not used. + optional sint32 position = 5 [default = -1]; + + // The player who owns the new zone the card is in + optional sint32 target_player_id = 6 [default = -1]; + + // The new zone the card is in + optional string target_zone = 7; + + // The new x coordinate (or new position for zones with no y coordinate) + optional sint32 x = 8 [default = -1]; + + // The new y coordinate + optional sint32 y = 9 [default = -1]; + + // The new id of the card if the card moved zone + optional sint32 new_card_id = 10 [default = -1]; + + // If the card is face down, face down cards will not show their name + optional bool face_down = 11; + + // The provider id of the card in case it was not known yet. + // Extends the name to supply a specific printing of that type of card. + optional string new_card_provider_id = 12; +} diff --git a/common/pb/event_notify_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto similarity index 100% rename from common/pb/event_notify_user.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_notify_user.proto diff --git a/common/pb/event_player_properties_changed.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_player_properties_changed.proto similarity index 100% rename from common/pb/event_player_properties_changed.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_player_properties_changed.proto diff --git a/common/pb/event_remove_from_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_from_list.proto similarity index 100% rename from common/pb/event_remove_from_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_from_list.proto diff --git a/common/pb/event_remove_messages.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_messages.proto similarity index 100% rename from common/pb/event_remove_messages.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_remove_messages.proto diff --git a/common/pb/event_replay_added.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_replay_added.proto similarity index 100% rename from common/pb/event_replay_added.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_replay_added.proto diff --git a/common/pb/event_reveal_cards.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reveal_cards.proto similarity index 100% rename from common/pb/event_reveal_cards.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_reveal_cards.proto diff --git a/common/pb/event_reverse_turn.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_reverse_turn.proto similarity index 100% rename from common/pb/event_reverse_turn.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_reverse_turn.proto diff --git a/common/pb/event_roll_die.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_roll_die.proto similarity index 100% rename from common/pb/event_roll_die.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_roll_die.proto diff --git a/common/pb/event_room_say.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_room_say.proto similarity index 100% rename from common/pb/event_room_say.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_room_say.proto diff --git a/common/pb/event_server_complete_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_complete_list.proto similarity index 100% rename from common/pb/event_server_complete_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_server_complete_list.proto diff --git a/common/pb/event_server_identification.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_identification.proto similarity index 100% rename from common/pb/event_server_identification.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_server_identification.proto diff --git a/common/pb/event_server_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_message.proto similarity index 100% rename from common/pb/event_server_message.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_server_message.proto diff --git a/common/pb/event_server_shutdown.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_server_shutdown.proto similarity index 100% rename from common/pb/event_server_shutdown.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_server_shutdown.proto diff --git a/common/pb/event_set_active_phase.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_phase.proto similarity index 100% rename from common/pb/event_set_active_phase.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_phase.proto diff --git a/common/pb/event_set_active_player.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_player.proto similarity index 100% rename from common/pb/event_set_active_player.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_set_active_player.proto diff --git a/common/pb/event_set_card_attr.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_attr.proto similarity index 100% rename from common/pb/event_set_card_attr.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_attr.proto diff --git a/common/pb/event_set_card_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_counter.proto similarity index 100% rename from common/pb/event_set_card_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_set_card_counter.proto diff --git a/common/pb/event_set_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter.proto similarity index 100% rename from common/pb/event_set_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_set_counter.proto diff --git a/common/pb/event_shuffle.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_shuffle.proto similarity index 100% rename from common/pb/event_shuffle.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_shuffle.proto diff --git a/common/pb/event_user_joined.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_joined.proto similarity index 100% rename from common/pb/event_user_joined.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_user_joined.proto diff --git a/common/pb/event_user_left.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_left.proto similarity index 100% rename from common/pb/event_user_left.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_user_left.proto diff --git a/common/pb/event_user_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/event_user_message.proto similarity index 100% rename from common/pb/event_user_message.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/event_user_message.proto diff --git a/common/pb/game_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto similarity index 82% rename from common/pb/game_commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto index b029d24ae..796f4fc68 100644 --- a/common/pb/game_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_commands.proto @@ -1,4 +1,6 @@ syntax = "proto2"; + +// Commands that are sent during a game to change the game state message GameCommand { enum GameCommandType { KICK_FROM_GAME = 1000; @@ -40,10 +42,15 @@ message GameCommand { extensions 100 to max; } +// A wrapper around a normal game command that allows a privileged user to send a command on behalf of another player message Command_Judge { extend GameCommand { optional Command_Judge ext = 1033; } + + // The player on whose behalf this command is sent optional sint32 target_id = 1 [default = -1]; + + // The wrapped game command repeated GameCommand game_command = 2; } diff --git a/common/pb/game_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto similarity index 92% rename from common/pb/game_event.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto index cc7584dcb..8682128af 100644 --- a/common/pb/game_event.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event.proto @@ -1,4 +1,6 @@ syntax = "proto2"; + +// Sent every time something happens in the game to update the client's state message GameEvent { enum GameEventType { JOIN = 1000; diff --git a/common/pb/game_event_container.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_container.proto similarity index 100% rename from common/pb/game_event_container.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/game_event_container.proto diff --git a/common/pb/game_event_context.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_event_context.proto similarity index 100% rename from common/pb/game_event_context.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/game_event_context.proto diff --git a/common/pb/game_replay.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/game_replay.proto similarity index 100% rename from common/pb/game_replay.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/game_replay.proto diff --git a/common/pb/isl_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/isl_message.proto similarity index 100% rename from common/pb/isl_message.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/isl_message.proto diff --git a/common/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto similarity index 100% rename from common/pb/moderator_commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto diff --git a/common/pb/move_card_to_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/move_card_to_zone.proto similarity index 100% rename from common/pb/move_card_to_zone.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/move_card_to_zone.proto diff --git a/common/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto similarity index 95% rename from common/pb/response.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index 18f4249c4..dece8ae17 100644 --- a/common/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -1,4 +1,6 @@ syntax = "proto2"; + +// Sent immediately after a command with the same cmd_id, connecting it to the command sent to the server message Response { enum ResponseCode { RespNotConnected = -1; @@ -65,6 +67,7 @@ message Response { GET_ADMIN_NOTES = 1018; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; + REPLAY_GET_CODE = 1102; } required uint64 cmd_id = 1; optional ResponseCode response_code = 2; diff --git a/common/pb/response_activate.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_activate.proto similarity index 100% rename from common/pb/response_activate.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_activate.proto diff --git a/common/pb/response_adjust_mod.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_adjust_mod.proto similarity index 100% rename from common/pb/response_adjust_mod.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_adjust_mod.proto diff --git a/common/pb/response_ban_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_ban_history.proto similarity index 100% rename from common/pb/response_ban_history.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_ban_history.proto diff --git a/common/pb/response_deck_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto similarity index 100% rename from common/pb/response_deck_download.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_download.proto diff --git a/common/pb/response_deck_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_list.proto similarity index 100% rename from common/pb/response_deck_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_list.proto diff --git a/common/pb/response_deck_upload.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_upload.proto similarity index 100% rename from common/pb/response_deck_upload.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_upload.proto diff --git a/common/pb/response_dump_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_dump_zone.proto similarity index 100% rename from common/pb/response_dump_zone.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_dump_zone.proto diff --git a/common/pb/response_forgotpasswordrequest.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_forgotpasswordrequest.proto similarity index 100% rename from common/pb/response_forgotpasswordrequest.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_forgotpasswordrequest.proto diff --git a/common/pb/response_get_admin_notes.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_admin_notes.proto similarity index 100% rename from common/pb/response_get_admin_notes.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_get_admin_notes.proto diff --git a/common/pb/response_get_games_of_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_games_of_user.proto similarity index 100% rename from common/pb/response_get_games_of_user.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_get_games_of_user.proto diff --git a/common/pb/response_get_user_info.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_get_user_info.proto similarity index 100% rename from common/pb/response_get_user_info.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_get_user_info.proto diff --git a/common/pb/response_join_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_join_room.proto similarity index 100% rename from common/pb/response_join_room.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_join_room.proto diff --git a/common/pb/response_list_users.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_list_users.proto similarity index 100% rename from common/pb/response_list_users.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_list_users.proto diff --git a/common/pb/response_login.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_login.proto similarity index 100% rename from common/pb/response_login.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_login.proto diff --git a/common/pb/response_password_salt.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_password_salt.proto similarity index 100% rename from common/pb/response_password_salt.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_password_salt.proto diff --git a/common/pb/response_register.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_register.proto similarity index 100% rename from common/pb/response_register.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_register.proto diff --git a/common/pb/response_replay_download.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download.proto similarity index 100% rename from common/pb/response_replay_download.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_download.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto new file mode 100644 index 000000000..5bb1b511a --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_get_code.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; +import "response.proto"; + +message Response_ReplayGetCode { + extend Response { + optional Response_ReplayGetCode ext = 1102; + } + optional string replay_code = 1; +} diff --git a/common/pb/response_replay_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_list.proto similarity index 100% rename from common/pb/response_replay_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_replay_list.proto diff --git a/common/pb/response_viewlog_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto similarity index 100% rename from common/pb/response_viewlog_history.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_viewlog_history.proto diff --git a/common/pb/response_warn_history.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_history.proto similarity index 100% rename from common/pb/response_warn_history.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_history.proto diff --git a/common/pb/response_warn_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto similarity index 100% rename from common/pb/response_warn_list.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/response_warn_list.proto diff --git a/common/pb/room_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto similarity index 63% rename from common/pb/room_commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto index 5e8c158ff..a8c90ec6c 100644 --- a/common/pb/room_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/room_commands.proto @@ -22,23 +22,52 @@ message Command_RoomSay { optional string message = 1; } +// Create a new game in the room message Command_CreateGame { extend RoomCommand { optional Command_CreateGame ext = 1002; } + + // game description shown in game list optional string description = 1; + + // password users will have to provide to join the game optional string password = 2; + + // amount of players needed to play optional uint32 max_players = 3; + + // limits the game to only allowing users on the creator's buddy list to join optional bool only_buddies = 4; + + // limits the game to only allowing registered users optional bool only_registered = 5; + + // allows non players to view the game as spectator optional bool spectators_allowed = 6; + + // allows spectators to join without password if false optional bool spectators_need_password = 7; + + // allows spectators to use game say commands optional bool spectators_can_talk = 8; + + // allows spectators to see hands and hidden information optional bool spectators_see_everything = 9; + + // selection of game types as presented in the server's room configuration repeated uint32 game_type_ids = 10; + + // the creator of the game will join it as a judge optional bool join_as_judge = 11; + + // the creator of the game will join it as a spectator optional bool join_as_spectator = 12; + + // set the starting life total optional uint32 starting_life_total = 13; + + // share decklists with all players when selected optional bool share_decklists_on_load = 14; } diff --git a/common/pb/room_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/room_event.proto similarity index 100% rename from common/pb/room_event.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/room_event.proto diff --git a/common/pb/server_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/server_message.proto similarity index 100% rename from common/pb/server_message.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/server_message.proto diff --git a/common/pb/serverinfo_arrow.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_arrow.proto similarity index 100% rename from common/pb/serverinfo_arrow.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_arrow.proto diff --git a/common/pb/serverinfo_ban.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_ban.proto similarity index 100% rename from common/pb/serverinfo_ban.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_ban.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto new file mode 100644 index 000000000..a9a8b5c9d --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_card.proto @@ -0,0 +1,56 @@ +syntax = "proto2"; +import "serverinfo_cardcounter.proto"; + +// Container for all the properties of a single card +message ServerInfo_Card { + // unique card id in this zone + optional sint32 id = 1 [default = -1]; + + // name of this kind of card + optional string name = 2; + + // x coordinate in zone + optional sint32 x = 3 [default = -1]; + + // y coordinate in zone + optional sint32 y = 4 [default = -1]; + + // if the card is face_down, hiding its information + optional bool face_down = 5; + + // if the card is tapped, turned sideways + optional bool tapped = 6; + + // if the card is marked as attacking + optional bool attacking = 7; + + // the card's color + optional string color = 8; + + // the power/toughness field, displayed on the bottom right + optional string pt = 9; + + // an optional string placed over the card + optional string annotation = 10; + + // whether the card should be deleted when removed from the table, like a token + optional bool destroy_on_zone_change = 11; + + // whether the card should not be untapped when the untap command is sent + optional bool doesnt_untap = 12; + + // list of counters on the card + repeated ServerInfo_CardCounter counter_list = 13; + + // the player that owns the card this is attached to + optional sint32 attach_player_id = 14 [default = -1]; + + // the zone of the card this is attached to + optional string attach_zone = 15; + + // the unique id of the card in that zone + optional sint32 attach_card_id = 16 [default = -1]; + + // unique id of this kind of card, extends the name to specify a specific printing of a card + optional string provider_id = 17; +} diff --git a/common/pb/serverinfo_cardcounter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cardcounter.proto similarity index 100% rename from common/pb/serverinfo_cardcounter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_cardcounter.proto diff --git a/common/pb/serverinfo_chat_message.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_chat_message.proto similarity index 100% rename from common/pb/serverinfo_chat_message.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_chat_message.proto diff --git a/common/pb/serverinfo_counter.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto similarity index 100% rename from common/pb/serverinfo_counter.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_counter.proto diff --git a/common/pb/serverinfo_deckstorage.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto similarity index 100% rename from common/pb/serverinfo_deckstorage.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_deckstorage.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto new file mode 100644 index 000000000..9a56e034c --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_game.proto @@ -0,0 +1,65 @@ +syntax = "proto2"; +import "serverinfo_user.proto"; + +// Container for information about a game in the room's game list +message ServerInfo_Game { + // id of server the game is on + optional sint32 server_id = 1 [default = -1]; + + // id of room the game is in + optional sint32 room_id = 2 [default = -1]; + + // unique id of the game inside the room + optional sint32 game_id = 3 [default = -1]; + + // user provided game description + optional string description = 4; + + // password required to join game + optional bool with_password = 5; + + // players required to play + optional uint32 max_players = 6; + + // mask of server defined game types + repeated sint32 game_types = 7; + + // user that created the game + optional ServerInfo_User creator_info = 8; + + // only buddies of the creator can join this game + optional bool only_buddies = 9; + + // only registered users can join this game + optional bool only_registered = 10; + + // if spectators are allowed to join + optional bool spectators_allowed = 11; + + // spectators need to enter the game + optional bool spectators_need_password = 12; + + // spectators can use cmdGameSay + optional bool spectators_can_chat = 13; + + // spectators receive private events for all players + optional bool spectators_omniscient = 14; + + // decklists are sent to all players when loaded + optional bool share_decklists_on_load = 15; + + // the current player count + optional uint32 player_count = 30; + + // the current spectator count + optional uint32 spectators_count = 31; + + // whether the game is currently ongoing + optional bool started = 50; + + // time that the game started at + optional uint32 start_time = 51; + + // whether the game is closed. Closed games are finished and can't be interacted with + optional bool closed = 52; +} diff --git a/common/pb/serverinfo_gametype.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_gametype.proto similarity index 100% rename from common/pb/serverinfo_gametype.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_gametype.proto diff --git a/common/pb/serverinfo_player.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto similarity index 100% rename from common/pb/serverinfo_player.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_player.proto diff --git a/common/pb/serverinfo_playerping.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerping.proto similarity index 100% rename from common/pb/serverinfo_playerping.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerping.proto diff --git a/common/pb/serverinfo_playerproperties.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto similarity index 100% rename from common/pb/serverinfo_playerproperties.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_playerproperties.proto diff --git a/common/pb/serverinfo_replay.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay.proto similarity index 100% rename from common/pb/serverinfo_replay.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay.proto diff --git a/common/pb/serverinfo_replay_match.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay_match.proto similarity index 100% rename from common/pb/serverinfo_replay_match.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_replay_match.proto diff --git a/common/pb/serverinfo_room.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_room.proto similarity index 100% rename from common/pb/serverinfo_room.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_room.proto diff --git a/common/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto similarity index 100% rename from common/pb/serverinfo_user.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto diff --git a/common/pb/serverinfo_warning.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_warning.proto similarity index 100% rename from common/pb/serverinfo_warning.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_warning.proto diff --git a/common/pb/serverinfo_zone.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_zone.proto similarity index 100% rename from common/pb/serverinfo_zone.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_zone.proto diff --git a/common/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto similarity index 98% rename from common/pb/session_commands.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index 64b3cd872..cecf87370 100644 --- a/common/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -31,6 +31,8 @@ message SessionCommand { REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; REPLAY_DELETE_MATCH = 1103; + REPLAY_GET_CODE = 1104; + REPLAY_SUBMIT_CODE = 1105; } extensions 100 to max; } diff --git a/common/pb/session_event.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_event.proto similarity index 100% rename from common/pb/session_event.proto rename to libcockatrice_protocol/libcockatrice/protocol/pb/session_event.proto diff --git a/cockatrice/src/server/pending_command.cpp b/libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp similarity index 100% rename from cockatrice/src/server/pending_command.cpp rename to libcockatrice_protocol/libcockatrice/protocol/pending_command.cpp diff --git a/cockatrice/src/server/pending_command.h b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h similarity index 77% rename from cockatrice/src/server/pending_command.h rename to libcockatrice_protocol/libcockatrice/protocol/pending_command.h index d845167ac..1d2d9ff17 100644 --- a/cockatrice/src/server/pending_command.h +++ b/libcockatrice_protocol/libcockatrice/protocol/pending_command.h @@ -1,10 +1,15 @@ +/** + * @file pending_command.h + * @ingroup Messages + * @brief TODO: Document this. + */ + #ifndef PENDING_COMMAND_H #define PENDING_COMMAND_H -#include "pb/commands.pb.h" -#include "pb/response.pb.h" - #include +#include +#include class PendingCommand : public QObject { diff --git a/libcockatrice_rng/CMakeLists.txt b/libcockatrice_rng/CMakeLists.txt new file mode 100644 index 000000000..6ff2a4537 --- /dev/null +++ b/libcockatrice_rng/CMakeLists.txt @@ -0,0 +1,20 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS libcockatrice/rng/rng_abstract.h libcockatrice/rng/rng_sfmt.h libcockatrice/rng/sfmt/SFMT.h) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_rng STATIC ${MOC_SOURCES} libcockatrice/rng/rng_abstract.cpp libcockatrice/rng/rng_sfmt.cpp + libcockatrice/rng/sfmt/SFMT.c +) + +target_include_directories(libcockatrice_rng PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_rng PUBLIC ${QT_CORE_MODULE}) diff --git a/common/rng_abstract.cpp b/libcockatrice_rng/libcockatrice/rng/rng_abstract.cpp similarity index 100% rename from common/rng_abstract.cpp rename to libcockatrice_rng/libcockatrice/rng/rng_abstract.cpp diff --git a/common/rng_abstract.h b/libcockatrice_rng/libcockatrice/rng/rng_abstract.h similarity index 84% rename from common/rng_abstract.h rename to libcockatrice_rng/libcockatrice/rng/rng_abstract.h index ffd45b9d7..903e6ef1a 100644 --- a/common/rng_abstract.h +++ b/libcockatrice_rng/libcockatrice/rng/rng_abstract.h @@ -13,7 +13,7 @@ public: } virtual unsigned int rand(int min, int max) = 0; QVector makeNumbersVector(int n, int min, int max); - double testRandom(const QVector &numbers) const; + [[nodiscard]] double testRandom(const QVector &numbers) const; }; extern RNG_Abstract *rng; diff --git a/common/rng_sfmt.cpp b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp similarity index 100% rename from common/rng_sfmt.cpp rename to libcockatrice_rng/libcockatrice/rng/rng_sfmt.cpp diff --git a/common/rng_sfmt.h b/libcockatrice_rng/libcockatrice/rng/rng_sfmt.h similarity index 100% rename from common/rng_sfmt.h rename to libcockatrice_rng/libcockatrice/rng/rng_sfmt.h diff --git a/common/sfmt/LICENSE.txt b/libcockatrice_rng/libcockatrice/rng/sfmt/LICENSE.txt similarity index 100% rename from common/sfmt/LICENSE.txt rename to libcockatrice_rng/libcockatrice/rng/sfmt/LICENSE.txt diff --git a/common/sfmt/SFMT-common.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-common.h similarity index 100% rename from common/sfmt/SFMT-common.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-common.h diff --git a/common/sfmt/SFMT-params.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params.h similarity index 100% rename from common/sfmt/SFMT-params.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params.h diff --git a/common/sfmt/SFMT-params19937.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params19937.h similarity index 100% rename from common/sfmt/SFMT-params19937.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT-params19937.h diff --git a/common/sfmt/SFMT.c b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.c similarity index 100% rename from common/sfmt/SFMT.c rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.c diff --git a/common/sfmt/SFMT.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h similarity index 100% rename from common/sfmt/SFMT.h rename to libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt new file mode 100644 index 000000000..3afe6e00a --- /dev/null +++ b/libcockatrice_settings/CMakeLists.txt @@ -0,0 +1,45 @@ +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(HEADERS + libcockatrice/settings/card_database_settings.h + libcockatrice/settings/card_override_settings.h + libcockatrice/settings/debug_settings.h + libcockatrice/settings/download_settings.h + libcockatrice/settings/game_filters_settings.h + libcockatrice/settings/layouts_settings.h + libcockatrice/settings/message_settings.h + libcockatrice/settings/recents_settings.h + libcockatrice/settings/servers_settings.h + libcockatrice/settings/settings_manager.h +) + +if(Qt6_FOUND) + qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) +elseif(Qt5_FOUND) + qt5_wrap_cpp(MOC_SOURCES ${HEADERS}) +endif() + +add_library( + libcockatrice_settings STATIC + ${MOC_SOURCES} + libcockatrice/settings/card_database_settings.cpp + libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/debug_settings.cpp + libcockatrice/settings/download_settings.cpp + libcockatrice/settings/game_filters_settings.cpp + libcockatrice/settings/layouts_settings.cpp + libcockatrice/settings/message_settings.cpp + libcockatrice/settings/recents_settings.cpp + libcockatrice/settings/servers_settings.cpp + libcockatrice/settings/settings_manager.cpp +) + +target_include_directories( + libcockatrice_settings + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR}/cockatrice/src/client/network +) + +target_link_libraries(libcockatrice_settings PUBLIC libcockatrice_card libcockatrice_utility ${QT_CORE_MODULE}) diff --git a/cockatrice/src/settings/card_database_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp similarity index 79% rename from cockatrice/src/settings/card_database_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp index f0b8d58b1..26a91a4dd 100644 --- a/cockatrice/src/settings/card_database_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.cpp @@ -1,7 +1,7 @@ #include "card_database_settings.h" CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "cardDatabase.ini", parent) + : SettingsManager(settingPath + "cardDatabase.ini", QString(), QString(), parent) { } @@ -20,17 +20,17 @@ void CardDatabaseSettings::setIsKnown(QString shortName, bool isknown) setValue(isknown, "isknown", "sets", std::move(shortName)); } -unsigned int CardDatabaseSettings::getSortKey(QString shortName) +unsigned int CardDatabaseSettings::getSortKey(QString shortName) const { return getValue("sortkey", "sets", std::move(shortName)).toUInt(); } -bool CardDatabaseSettings::isEnabled(QString shortName) +bool CardDatabaseSettings::isEnabled(QString shortName) const { return getValue("enabled", "sets", std::move(shortName)).toBool(); } -bool CardDatabaseSettings::isKnown(QString shortName) +bool CardDatabaseSettings::isKnown(QString shortName) const { return getValue("isknown", "sets", std::move(shortName)).toBool(); } diff --git a/libcockatrice_settings/libcockatrice/settings/card_database_settings.h b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h new file mode 100644 index 000000000..bb946ea80 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/card_database_settings.h @@ -0,0 +1,33 @@ +/** + * @file card_database_settings.h + * @ingroup CardDatabase + * @ingroup CardSettings + * @brief TODO: Document this. + */ + +#ifndef CARDDATABASESETTINGS_H +#define CARDDATABASESETTINGS_H + +#include "settings_manager.h" + +#include + +class CardDatabaseSettings : public SettingsManager, public ICardSetPriorityController +{ + Q_OBJECT + friend class SettingsCache; + +public: + void setSortKey(QString shortName, unsigned int sortKey) override; + void setEnabled(QString shortName, bool enabled) override; + void setIsKnown(QString shortName, bool isknown) override; + + unsigned int getSortKey(QString shortName) const override; + bool isEnabled(QString shortName) const override; + bool isKnown(QString shortName) const override; + +private: + explicit CardDatabaseSettings(const QString &settingPath, QObject *parent = nullptr); +}; + +#endif // CARDDATABASESETTINGS_H diff --git a/cockatrice/src/settings/card_override_settings.cpp b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp similarity index 71% rename from cockatrice/src/settings/card_override_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp index 2e2c7607d..a61a4693b 100644 --- a/cockatrice/src/settings/card_override_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.cpp @@ -1,21 +1,21 @@ #include "card_override_settings.h" CardOverrideSettings::CardOverrideSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "cardPreferenceOverrides.ini", parent) + : SettingsManager(settingPath + "cardPreferenceOverrides.ini", "cards", QString(), parent) { } void CardOverrideSettings::setCardPreferenceOverride(const CardRef &cardRef) { - setValue(cardRef.providerId, cardRef.name, "cards"); + setValue(cardRef.providerId, cardRef.name); } void CardOverrideSettings::deleteCardPreferenceOverride(const QString &cardName) { - deleteValue(cardName, "cards"); + deleteValue(cardName); } -QString CardOverrideSettings::getCardPreferenceOverride(const QString &cardName) +QString CardOverrideSettings::getCardPreferenceOverride(const QString &cardName) const { - return getValue(cardName, "cards").toString(); + return getValue(cardName).toString(); } \ No newline at end of file diff --git a/cockatrice/src/settings/card_override_settings.h b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h similarity index 73% rename from cockatrice/src/settings/card_override_settings.h rename to libcockatrice_settings/libcockatrice/settings/card_override_settings.h index 520d9f65f..3d9db4e65 100644 --- a/cockatrice/src/settings/card_override_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/card_override_settings.h @@ -1,10 +1,16 @@ +/** + * @file card_override_settings.h + * @ingroup CardSettings + * @brief TODO: Document this. + */ + #ifndef COCKATRICE_CARD_OVERRIDE_SETTINGS_H #define COCKATRICE_CARD_OVERRIDE_SETTINGS_H -#include "../common/card_ref.h" #include "settings_manager.h" #include +#include class CardOverrideSettings : public SettingsManager { @@ -16,7 +22,7 @@ public: void deleteCardPreferenceOverride(const QString &cardName); - QString getCardPreferenceOverride(const QString &cardName); + QString getCardPreferenceOverride(const QString &cardName) const; private: explicit CardOverrideSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/cockatrice/src/settings/debug_settings.cpp b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp similarity index 68% rename from cockatrice/src/settings/debug_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/debug_settings.cpp index f6f12f60e..5bf6eca30 100644 --- a/cockatrice/src/settings/debug_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.cpp @@ -3,7 +3,7 @@ #include DebugSettings::DebugSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "debug.ini", parent) + : SettingsManager(settingPath + "debug.ini", "debug", QString(), parent) { // Create the default debug.ini if it doesn't exist yet if (!QFile(settingPath + "debug.ini").exists()) { @@ -11,22 +11,22 @@ DebugSettings::DebugSettings(const QString &settingPath, QObject *parent) } } -bool DebugSettings::getShowCardId() +bool DebugSettings::getShowCardId() const { - return getValue("showCardId", "debug").toBool(); + return getValue("showCardId").toBool(); } -bool DebugSettings::getLocalGameOnStartup() +bool DebugSettings::getLocalGameOnStartup() const { return getValue("onStartup", "localgame").toBool(); } -int DebugSettings::getLocalGamePlayerCount() +int DebugSettings::getLocalGamePlayerCount() const { return getValue("playerCount", "localgame").toInt(); } -QString DebugSettings::getDeckPathForPlayer(const QString &playerName) +QString DebugSettings::getDeckPathForPlayer(const QString &playerName) const { return getValue(playerName, "localgame", "deck").toString(); } \ No newline at end of file diff --git a/cockatrice/src/settings/debug_settings.h b/libcockatrice_settings/libcockatrice/settings/debug_settings.h similarity index 56% rename from cockatrice/src/settings/debug_settings.h rename to libcockatrice_settings/libcockatrice/settings/debug_settings.h index 89ea33e4e..30cdd5fa5 100644 --- a/cockatrice/src/settings/debug_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/debug_settings.h @@ -1,3 +1,9 @@ +/** + * @file debug_settings.h + * @ingroup CoreSettings + * @brief TODO: Document this. + */ + #ifndef DEBUG_SETTINGS_H #define DEBUG_SETTINGS_H #include "settings_manager.h" @@ -11,12 +17,12 @@ class DebugSettings : public SettingsManager DebugSettings(const DebugSettings & /*other*/); public: - bool getShowCardId(); + bool getShowCardId() const; - bool getLocalGameOnStartup(); - int getLocalGamePlayerCount(); + bool getLocalGameOnStartup() const; + int getLocalGamePlayerCount() const; - QString getDeckPathForPlayer(const QString &playerName); + QString getDeckPathForPlayer(const QString &playerName) const; }; #endif // DEBUG_SETTINGS_H diff --git a/cockatrice/src/settings/download_settings.cpp b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp similarity index 68% rename from cockatrice/src/settings/download_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/download_settings.cpp index adbe26363..66525a598 100644 --- a/cockatrice/src/settings/download_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.cpp @@ -9,21 +9,21 @@ const QStringList DownloadSettings::DEFAULT_DOWNLOAD_URLS = { "https://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"}; DownloadSettings::DownloadSettings(const QString &settingPath, QObject *parent = nullptr) - : SettingsManager(settingPath + "downloads.ini", parent) + : SettingsManager(settingPath + "downloads.ini", "downloads", QString(), parent) { } void DownloadSettings::setDownloadUrls(const QStringList &downloadURLs) { - setValue(QVariant::fromValue(downloadURLs), "urls", "downloads"); + setValue(QVariant::fromValue(downloadURLs), "urls"); } -QStringList DownloadSettings::getAllURLs() +QStringList DownloadSettings::getAllURLs() const { - return getValue("urls", "downloads").toStringList(); + return getValue("urls").toStringList(); } void DownloadSettings::resetToDefaultURLs() { - setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls", "downloads"); + setValue(QVariant::fromValue(DEFAULT_DOWNLOAD_URLS), "urls"); } diff --git a/cockatrice/src/settings/download_settings.h b/libcockatrice_settings/libcockatrice/settings/download_settings.h similarity index 77% rename from cockatrice/src/settings/download_settings.h rename to libcockatrice_settings/libcockatrice/settings/download_settings.h index a961199b3..b7442301e 100644 --- a/cockatrice/src/settings/download_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/download_settings.h @@ -1,10 +1,14 @@ +/** + * @file download_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + #ifndef COCKATRICE_DOWNLOADSETTINGS_H #define COCKATRICE_DOWNLOADSETTINGS_H #include "settings_manager.h" -#include - class DownloadSettings : public SettingsManager { Q_OBJECT @@ -15,7 +19,7 @@ class DownloadSettings : public SettingsManager public: explicit DownloadSettings(const QString &, QObject *); - QStringList getAllURLs(); + QStringList getAllURLs() const; void setDownloadUrls(const QStringList &downloadURLs); void resetToDefaultURLs(); }; diff --git a/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp new file mode 100644 index 000000000..4f5bf52ee --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.cpp @@ -0,0 +1,208 @@ +#include "game_filters_settings.h" + +#include +#include + +GameFiltersSettings::GameFiltersSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "gamefilters.ini", "filter_games", QString(), parent) +{ +} + +/** + * The game type might contain special characters, so to use it in + * QSettings we just hash it. + */ +static QString hashGameType(const QString &gameType) +{ + return QCryptographicHash::hash(gameType.toUtf8(), QCryptographicHash::Md5).toHex(); +} + +void GameFiltersSettings::setHideBuddiesOnlyGames(bool hide) +{ + setValue(hide, "hide_buddies_only_games"); +} + +bool GameFiltersSettings::isHideBuddiesOnlyGames() const +{ + QVariant previous = getValue("hide_buddies_only_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideFullGames(bool hide) +{ + setValue(hide, "hide_full_games"); +} + +bool GameFiltersSettings::isHideFullGames() const +{ + QVariant previous = getValue("hide_full_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideGamesThatStarted(bool hide) +{ + setValue(hide, "hide_games_that_started"); +} + +bool GameFiltersSettings::isHideGamesThatStarted() const +{ + QVariant previous = getValue("hide_games_that_started"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHidePasswordProtectedGames(bool hide) +{ + setValue(hide, "hide_password_protected_games"); +} + +bool GameFiltersSettings::isHidePasswordProtectedGames() const +{ + QVariant previous = getValue("hide_password_protected_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideIgnoredUserGames(bool hide) +{ + setValue(hide, "hide_ignored_user_games"); +} + +bool GameFiltersSettings::isHideIgnoredUserGames() const +{ + QVariant previous = getValue("hide_ignored_user_games"); + return previous == QVariant() ? true : previous.toBool(); +} + +void GameFiltersSettings::setHideNotBuddyCreatedGames(bool hide) +{ + setValue(hide, "hide_not_buddy_created_games"); +} + +bool GameFiltersSettings::isHideNotBuddyCreatedGames() const +{ + QVariant previous = getValue("hide_not_buddy_created_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setHideOpenDecklistGames(bool hide) +{ + setValue(hide, "hide_open_decklist_games"); +} + +bool GameFiltersSettings::isHideOpenDecklistGames() const +{ + QVariant previous = getValue("hide_open_decklist_games"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setGameNameFilter(QString gameName) +{ + setValue(gameName, "game_name_filter"); +} + +QString GameFiltersSettings::getGameNameFilter() const +{ + return getValue("game_name_filter").toString(); +} + +void GameFiltersSettings::setCreatorNameFilters(QStringList creatorName) +{ + setValue(creatorName, "creator_name_filter"); +} + +QStringList GameFiltersSettings::getCreatorNameFilters() const +{ + return getValue("creator_name_filter").toStringList(); +} + +void GameFiltersSettings::setMinPlayers(int min) +{ + setValue(min, "min_players"); +} + +int GameFiltersSettings::getMinPlayers() const +{ + QVariant previous = getValue("min_players"); + return previous == QVariant() ? 1 : previous.toInt(); +} + +void GameFiltersSettings::setMaxPlayers(int max) +{ + setValue(max, "max_players"); +} + +int GameFiltersSettings::getMaxPlayers() const +{ + QVariant previous = getValue("max_players"); + return previous == QVariant() ? 99 : previous.toInt(); +} + +void GameFiltersSettings::setMaxGameAge(const QTime &maxGameAge) +{ + setValue(maxGameAge, "max_game_age_time"); +} + +QTime GameFiltersSettings::getMaxGameAge() const +{ + QVariant previous = getValue("max_game_age_time"); + return previous.toTime(); +} + +void GameFiltersSettings::setGameTypeEnabled(QString gametype, bool enabled) +{ + setValue(enabled, "game_type/" + hashGameType(gametype)); +} + +void GameFiltersSettings::setGameHashedTypeEnabled(QString gametypeHASHED, bool enabled) +{ + setValue(enabled, gametypeHASHED); +} + +bool GameFiltersSettings::isGameTypeEnabled(QString gametype) const +{ + QVariant previous = getValue("game_type/" + hashGameType(gametype)); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanWatch(bool show) +{ + setValue(show, "show_only_if_spectators_can_watch"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanWatch() const +{ + QVariant previous = getValue("show_only_if_spectators_can_watch"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowSpectatorPasswordProtected(bool show) +{ + setValue(show, "show_spectator_password_protected"); +} + +bool GameFiltersSettings::isShowSpectatorPasswordProtected() const +{ + QVariant previous = getValue("show_spectator_password_protected"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanChat(bool show) +{ + setValue(show, "show_only_if_spectators_can_chat"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanChat() const +{ + QVariant previous = getValue("show_only_if_spectators_can_chat"); + return previous == QVariant() ? false : previous.toBool(); +} + +void GameFiltersSettings::setShowOnlyIfSpectatorsCanSeeHands(bool show) +{ + setValue(show, "show_only_if_spectators_can_see_hands"); +} + +bool GameFiltersSettings::isShowOnlyIfSpectatorsCanSeeHands() const +{ + QVariant previous = getValue("show_only_if_spectators_can_see_hands"); + return previous == QVariant() ? false : previous.toBool(); +} \ No newline at end of file diff --git a/cockatrice/src/settings/game_filters_settings.h b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h similarity index 57% rename from cockatrice/src/settings/game_filters_settings.h rename to libcockatrice_settings/libcockatrice/settings/game_filters_settings.h index a243c74e9..c0e60551a 100644 --- a/cockatrice/src/settings/game_filters_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/game_filters_settings.h @@ -1,3 +1,10 @@ +/** + * @file game_filters_settings.h + * @ingroup Lobby + * @ingroup GameSettings + * @brief TODO: Document this. + */ + #ifndef GAMEFILTERSSETTINGS_H #define GAMEFILTERSSETTINGS_H @@ -9,33 +16,33 @@ class GameFiltersSettings : public SettingsManager friend class SettingsCache; public: - bool isHideBuddiesOnlyGames(); - bool isHideFullGames(); - bool isHideGamesThatStarted(); - bool isHidePasswordProtectedGames(); - bool isHideIgnoredUserGames(); - bool isHideNotBuddyCreatedGames(); - void setHideOpenDecklistGames(bool hide); - bool isHideOpenDecklistGames(); - QString getGameNameFilter(); - QString getCreatorNameFilter(); - int getMinPlayers(); - int getMaxPlayers(); - QTime getMaxGameAge(); - bool isGameTypeEnabled(QString gametype); - bool isShowOnlyIfSpectatorsCanWatch(); - bool isShowSpectatorPasswordProtected(); - bool isShowOnlyIfSpectatorsCanChat(); - bool isShowOnlyIfSpectatorsCanSeeHands(); + bool isHideBuddiesOnlyGames() const; + bool isHideFullGames() const; + bool isHideGamesThatStarted() const; + bool isHidePasswordProtectedGames() const; + bool isHideIgnoredUserGames() const; + bool isHideNotBuddyCreatedGames() const; + bool isHideOpenDecklistGames() const; + QString getGameNameFilter() const; + QStringList getCreatorNameFilters() const; + int getMinPlayers() const; + int getMaxPlayers() const; + QTime getMaxGameAge() const; + bool isGameTypeEnabled(QString gametype) const; + bool isShowOnlyIfSpectatorsCanWatch() const; + bool isShowSpectatorPasswordProtected() const; + bool isShowOnlyIfSpectatorsCanChat() const; + bool isShowOnlyIfSpectatorsCanSeeHands() const; void setHideBuddiesOnlyGames(bool hide); void setHideIgnoredUserGames(bool hide); + void setHideOpenDecklistGames(bool hide); void setHideFullGames(bool hide); void setHideGamesThatStarted(bool hide); void setHidePasswordProtectedGames(bool hide); void setHideNotBuddyCreatedGames(bool hide); void setGameNameFilter(QString gameName); - void setCreatorNameFilter(QString creatorName); + void setCreatorNameFilters(QStringList creatorName); void setMinPlayers(int min); void setMaxPlayers(int max); void setMaxGameAge(const QTime &maxGameAge); @@ -45,15 +52,10 @@ public: void setShowSpectatorPasswordProtected(bool show); void setShowOnlyIfSpectatorsCanChat(bool show); void setShowOnlyIfSpectatorsCanSeeHands(bool show); -signals: - -public slots: private: explicit GameFiltersSettings(const QString &settingPath, QObject *parent = nullptr); GameFiltersSettings(const GameFiltersSettings & /*other*/); - - QString hashGameType(const QString &gameType) const; }; #endif // GAMEFILTERSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp new file mode 100644 index 000000000..e914dc2d8 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.cpp @@ -0,0 +1,149 @@ +#include "layouts_settings.h" + +const static QString STATE_PROP = "state"; +const static QString GEOMETRY_PROP = "geometry"; +const static QString SIZE_PROP = "widgetSize"; + +const static QString GROUP_MAIN_WINDOW = "mainWindow"; +const static QString GROUP_DECK_EDITOR = "deckEditor"; +const static QString GROUP_VISUAL_DECK_EDITOR = "visualDeckEditor"; +const static QString GROUP_DECK_EDITOR_DB = "deckEditorDb"; +const static QString GROUP_SETS_DIALOG = "setsDialog"; +const static QString GROUP_TOKEN_DIALOG = "tokenDialog"; +const static QString GROUP_GAME_PLAY_AREA = "gamePlayArea"; +const static QString GROUP_REPLAY_PLAY_AREA = "replayPlayArea"; + +LayoutsSettings::LayoutsSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "layouts.ini", QString(), QString(), parent) +{ +} + +void LayoutsSettings::setMainWindowGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_MAIN_WINDOW); +} + +QByteArray LayoutsSettings::getMainWindowGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_MAIN_WINDOW).toByteArray(); +} + +QByteArray LayoutsSettings::getDeckEditorLayoutState() const +{ + return getValue(STATE_PROP, GROUP_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setDeckEditorLayoutState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getDeckEditorGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setDeckEditorGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getVisualDeckEditorLayoutState() const +{ + return getValue(STATE_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setVisualDeckEditorLayoutState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_VISUAL_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getVisualDeckEditorGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR).toByteArray(); +} + +void LayoutsSettings::setVisualDeckEditorGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_VISUAL_DECK_EDITOR); +} + +QByteArray LayoutsSettings::getDeckEditorDbHeaderState() const +{ + return getValue(STATE_PROP, GROUP_DECK_EDITOR_DB, "header").toByteArray(); +} + +void LayoutsSettings::setDeckEditorDbHeaderState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_DECK_EDITOR_DB, "header"); +} + +QByteArray LayoutsSettings::getSetsDialogHeaderState() const +{ + return getValue(STATE_PROP, GROUP_SETS_DIALOG, "header").toByteArray(); +} + +void LayoutsSettings::setSetsDialogHeaderState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_SETS_DIALOG, "header"); +} + +void LayoutsSettings::setSetsDialogGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_SETS_DIALOG); +} + +QByteArray LayoutsSettings::getSetsDialogGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_SETS_DIALOG).toByteArray(); +} + +void LayoutsSettings::setTokenDialogGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_TOKEN_DIALOG); +} + +QByteArray LayoutsSettings::getTokenDialogGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_TOKEN_DIALOG).toByteArray(); +} + +void LayoutsSettings::setGamePlayAreaGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_GAME_PLAY_AREA); +} + +void LayoutsSettings::setGamePlayAreaState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_GAME_PLAY_AREA); +} + +QByteArray LayoutsSettings::getGamePlayAreaLayoutState() const +{ + return getValue(STATE_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); +} + +QByteArray LayoutsSettings::getGamePlayAreaGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_GAME_PLAY_AREA).toByteArray(); +} + +void LayoutsSettings::setReplayPlayAreaGeometry(const QByteArray &value) +{ + setValue(value, GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA); +} + +void LayoutsSettings::setReplayPlayAreaState(const QByteArray &value) +{ + setValue(value, STATE_PROP, GROUP_REPLAY_PLAY_AREA); +} + +QByteArray LayoutsSettings::getReplayPlayAreaLayoutState() const +{ + return getValue(STATE_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); +} + +QByteArray LayoutsSettings::getReplayPlayAreaGeometry() const +{ + return getValue(GEOMETRY_PROP, GROUP_REPLAY_PLAY_AREA).toByteArray(); +} diff --git a/libcockatrice_settings/libcockatrice/settings/layouts_settings.h b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h new file mode 100644 index 000000000..5353ce15a --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/layouts_settings.h @@ -0,0 +1,66 @@ +/** + * @file layouts_settings.h + * @ingroup CoreSettings + * @brief TODO: Document this. + */ + +#ifndef LAYOUTSSETTINGS_H +#define LAYOUTSSETTINGS_H + +#include "settings_manager.h" + +#include + +class LayoutsSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + void setMainWindowGeometry(const QByteArray &value); + + void setDeckEditorLayoutState(const QByteArray &value); + void setDeckEditorGeometry(const QByteArray &value); + + void setVisualDeckEditorLayoutState(const QByteArray &value); + void setVisualDeckEditorGeometry(const QByteArray &value); + + void setDeckEditorDbHeaderState(const QByteArray &value); + void setSetsDialogHeaderState(const QByteArray &value); + void setSetsDialogGeometry(const QByteArray &value); + void setTokenDialogGeometry(const QByteArray &value); + + void setGamePlayAreaGeometry(const QByteArray &value); + void setGamePlayAreaState(const QByteArray &value); + + void setReplayPlayAreaGeometry(const QByteArray &value); + void setReplayPlayAreaState(const QByteArray &value); + + QByteArray getMainWindowGeometry() const; + + QByteArray getDeckEditorLayoutState() const; + QByteArray getDeckEditorGeometry() const; + + QByteArray getVisualDeckEditorLayoutState() const; + QByteArray getVisualDeckEditorGeometry() const; + + QByteArray getDeckEditorDbHeaderState() const; + QByteArray getSetsDialogHeaderState() const; + QByteArray getSetsDialogGeometry() const; + QByteArray getTokenDialogGeometry() const; + + QByteArray getGamePlayAreaLayoutState() const; + QByteArray getGamePlayAreaGeometry() const; + + QByteArray getReplayPlayAreaLayoutState() const; + QByteArray getReplayPlayAreaGeometry() const; +signals: + +public slots: + +private: + explicit LayoutsSettings(const QString &settingPath, QObject *parent = nullptr); + LayoutsSettings(const LayoutsSettings & /*other*/); +}; + +#endif // LAYOUTSSETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/message_settings.cpp b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp new file mode 100644 index 000000000..50da39df6 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.cpp @@ -0,0 +1,26 @@ +#include "message_settings.h" + +MessageSettings::MessageSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "messages.ini", "messages", QString(), parent) +{ +} + +QString MessageSettings::getMessageAt(int index) const +{ + return getValue(QString("msg%1").arg(index)).toString(); +} + +int MessageSettings::getCount() const +{ + return getValue("count").toInt(); +} + +void MessageSettings::setCount(int count) +{ + setValue(count, "count"); +} + +void MessageSettings::setMessageAt(int index, QString message) +{ + setValue(message, QString("msg%1").arg(index)); +} diff --git a/cockatrice/src/settings/message_settings.h b/libcockatrice_settings/libcockatrice/settings/message_settings.h similarity index 75% rename from cockatrice/src/settings/message_settings.h rename to libcockatrice_settings/libcockatrice/settings/message_settings.h index 6d65ce6e8..ec70027af 100644 --- a/cockatrice/src/settings/message_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/message_settings.h @@ -1,3 +1,9 @@ +/** + * @file message_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + #ifndef MESSAGESETTINGS_H #define MESSAGESETTINGS_H @@ -9,8 +15,8 @@ class MessageSettings : public SettingsManager friend class SettingsCache; public: - int getCount(); - QString getMessageAt(int index); + int getCount() const; + QString getMessageAt(int index) const; void setCount(int count); void setMessageAt(int index, QString message); diff --git a/cockatrice/src/settings/recents_settings.cpp b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp similarity index 64% rename from cockatrice/src/settings/recents_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/recents_settings.cpp index 5bbec3c8c..76bc4069e 100644 --- a/cockatrice/src/settings/recents_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.cpp @@ -3,22 +3,22 @@ #define MAX_RECENT_DECK_COUNT 10 RecentsSettings::RecentsSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "recents.ini", parent) + : SettingsManager(settingPath + "recents.ini", "deckbuilder", QString(), parent) { } -QStringList RecentsSettings::getRecentlyOpenedDeckPaths() +QStringList RecentsSettings::getRecentlyOpenedDeckPaths() const { - return getValue("deckpaths", "deckbuilder").toStringList(); + return getValue("deckpaths").toStringList(); } void RecentsSettings::clearRecentlyOpenedDeckPaths() { - deleteValue("deckpaths", "deckbuilder"); + deleteValue("deckpaths"); emit recentlyOpenedDeckPathsChanged(); } void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) { - auto deckPaths = getValue("deckpaths", "deckbuilder").toStringList(); + auto deckPaths = getValue("deckpaths").toStringList(); deckPaths.removeAll(deckPath); deckPaths.prepend(deckPath); @@ -27,11 +27,11 @@ void RecentsSettings::updateRecentlyOpenedDeckPaths(const QString &deckPath) deckPaths.removeLast(); } - setValue(deckPaths, "deckpaths", "deckbuilder"); + setValue(deckPaths, "deckpaths"); emit recentlyOpenedDeckPathsChanged(); } -QString RecentsSettings::getLatestDeckDirPath() +QString RecentsSettings::getLatestDeckDirPath() const { return getValue("latestDeckDir", "dirs").toString(); } diff --git a/cockatrice/src/settings/recents_settings.h b/libcockatrice_settings/libcockatrice/settings/recents_settings.h similarity index 75% rename from cockatrice/src/settings/recents_settings.h rename to libcockatrice_settings/libcockatrice/settings/recents_settings.h index 6e91cd2f0..3aebff334 100644 --- a/cockatrice/src/settings/recents_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/recents_settings.h @@ -1,3 +1,9 @@ +/** + * @file recents_settings.h + * @ingroup DeckSettings + * @brief TODO: Document this. + */ + #ifndef RECENTS_SETTINGS_H #define RECENTS_SETTINGS_H @@ -12,11 +18,11 @@ class RecentsSettings : public SettingsManager RecentsSettings(const RecentsSettings & /*other*/); public: - QStringList getRecentlyOpenedDeckPaths(); + QStringList getRecentlyOpenedDeckPaths() const; void clearRecentlyOpenedDeckPaths(); void updateRecentlyOpenedDeckPaths(const QString &deckPath); - QString getLatestDeckDirPath(); + QString getLatestDeckDirPath() const; void setLatestDeckDirPath(const QString &dirPath); signals: diff --git a/cockatrice/src/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp similarity index 86% rename from cockatrice/src/settings/servers_settings.cpp rename to libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index 01a6c795b..0140182be 100644 --- a/cockatrice/src/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -4,34 +4,34 @@ #include ServersSettings::ServersSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "servers.ini", parent) + : SettingsManager(settingPath + "servers.ini", "server", QString(), parent) { } void ServersSettings::setPreviousHostLogin(int previous) { - setValue(previous, "previoushostlogin", "server"); + setValue(previous, "previoushostlogin"); } -int ServersSettings::getPreviousHostLogin() +int ServersSettings::getPreviousHostLogin() const { - QVariant previous = getValue("previoushostlogin", "server"); + QVariant previous = getValue("previoushostlogin"); return previous == QVariant() ? 1 : previous.toInt(); } void ServersSettings::setPreviousHostList(QStringList list) { - setValue(list, "previoushosts", "server"); + setValue(list, "previoushosts"); } -QStringList ServersSettings::getPreviousHostList() +QStringList ServersSettings::getPreviousHostList() const { - return getValue("previoushosts", "server").toStringList(); + return getValue("previoushosts").toStringList(); } void ServersSettings::setPrevioushostName(const QString &name) { - setValue(name, "previoushostName", "server"); + setValue(name, "previoushostName"); } QString ServersSettings::getSaveName(QString defaultname) @@ -48,13 +48,13 @@ QString ServersSettings::getSite(QString defaultSite) return site == QVariant() ? std::move(defaultSite) : site.toString(); } -QString ServersSettings::getPrevioushostName() +QString ServersSettings::getPrevioushostName() const { - QVariant value = getValue("previoushostName", "server"); + QVariant value = getValue("previoushostName"); return value == QVariant() ? "Rooster Ranges" : value.toString(); } -int ServersSettings::getPrevioushostindex(const QString &saveName) +int ServersSettings::getPrevioushostindex(const QString &saveName) const { int size = getValue("totalServers", "server", "server_details").toInt(); @@ -65,14 +65,14 @@ int ServersSettings::getPrevioushostindex(const QString &saveName) return -1; } -QString ServersSettings::getHostname(QString defaultHost) +QString ServersSettings::getHostname(QString defaultHost) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant hostname = getValue(QString("server%1").arg(index), "server", "server_details"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } -QString ServersSettings::getPort(QString defaultPort) +QString ServersSettings::getPort(QString defaultPort) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant port = getValue(QString("port%1").arg(index), "server", "server_details"); @@ -80,7 +80,7 @@ QString ServersSettings::getPort(QString defaultPort) return port == QVariant() ? std::move(defaultPort) : port.toString(); } -QString ServersSettings::getPlayerName(QString defaultName) +QString ServersSettings::getPlayerName(QString defaultName) const { int index = getPrevioushostindex(getPrevioushostName()); QVariant name = getValue(QString("username%1").arg(index), "server", "server_details"); @@ -98,7 +98,7 @@ QString ServersSettings::getPassword() return QString(); } -bool ServersSettings::getSavePassword() +bool ServersSettings::getSavePassword() const { int index = getPrevioushostindex(getPrevioushostName()); bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); @@ -107,56 +107,56 @@ bool ServersSettings::getSavePassword() void ServersSettings::setAutoConnect(int autoconnect) { - setValue(autoconnect, "auto_connect", "server"); + setValue(autoconnect, "auto_connect"); } -int ServersSettings::getAutoConnect() +int ServersSettings::getAutoConnect() const { - QVariant autoconnect = getValue("auto_connect", "server"); + QVariant autoconnect = getValue("auto_connect"); return autoconnect == QVariant() ? 0 : autoconnect.toInt(); } void ServersSettings::setFPHostName(QString hostname) { - setValue(hostname, "fphostname", "server"); + setValue(hostname, "fphostname"); } -QString ServersSettings::getFPHostname(QString defaultHost) +QString ServersSettings::getFPHostname(QString defaultHost) const { - QVariant hostname = getValue("fphostname", "server"); + QVariant hostname = getValue("fphostname"); return hostname == QVariant() ? std::move(defaultHost) : hostname.toString(); } void ServersSettings::setFPPort(QString port) { - setValue(port, "fpport", "server"); + setValue(port, "fpport"); } -QString ServersSettings::getFPPort(QString defaultPort) +QString ServersSettings::getFPPort(QString defaultPort) const { - QVariant port = getValue("fpport", "server"); + QVariant port = getValue("fpport"); return port == QVariant() ? std::move(defaultPort) : port.toString(); } void ServersSettings::setFPPlayerName(QString playerName) { - setValue(playerName, "fpplayername", "server"); + setValue(playerName, "fpplayername"); } -QString ServersSettings::getFPPlayerName(QString defaultName) +QString ServersSettings::getFPPlayerName(QString defaultName) const { - QVariant name = getValue("fpplayername", "server"); + QVariant name = getValue("fpplayername"); return name == QVariant() ? std::move(defaultName) : name.toString(); } void ServersSettings::setClearDebugLogStatus(bool abIsChecked) { - setValue(abIsChecked, "save_debug_log", "server"); + setValue(abIsChecked, "save_debug_log"); } -bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) +bool ServersSettings::getClearDebugLogStatus(bool abDefaultValue) const { - QVariant cbFlushLog = getValue("save_debug_log", "server"); + QVariant cbFlushLog = getValue("save_debug_log"); return cbFlushLog == QVariant() ? abDefaultValue : cbFlushLog.toBool(); } diff --git a/cockatrice/src/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h similarity index 79% rename from cockatrice/src/settings/servers_settings.h rename to libcockatrice_settings/libcockatrice/settings/servers_settings.h index 9bf5c47d7..22603a356 100644 --- a/cockatrice/src/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -1,3 +1,9 @@ +/** + * @file servers_settings.h + * @ingroup NetworkSettings + * @brief TODO: Document this. + */ + #ifndef SERVERSSETTINGS_H #define SERVERSSETTINGS_H @@ -16,21 +22,21 @@ class ServersSettings : public SettingsManager friend class SettingsCache; public: - int getPreviousHostLogin(); - int getPrevioushostindex(const QString &); - QStringList getPreviousHostList(); - QString getPrevioushostName(); - QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); - QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); - QString getPlayerName(QString defaultName = ""); - QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST); - QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT); - QString getFPPlayerName(QString defaultName = ""); + int getPreviousHostLogin() const; + int getPrevioushostindex(const QString &) const; + QStringList getPreviousHostList() const; + QString getPrevioushostName() const; + QString getHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getPlayerName(QString defaultName = "") const; + QString getFPHostname(QString defaultHost = SERVERSETTINGS_DEFAULT_HOST) const; + QString getFPPort(QString defaultPort = SERVERSETTINGS_DEFAULT_PORT) const; + QString getFPPlayerName(QString defaultName = "") const; QString getPassword(); QString getSaveName(QString defaultname = ""); QString getSite(QString defaultName = ""); - bool getSavePassword(); - int getAutoConnect(); + bool getSavePassword() const; + int getAutoConnect() const; void setPreviousHostLogin(int previous); void setPrevioushostName(const QString &); @@ -61,7 +67,7 @@ public: QString port = QString(), QString site = QString()); void setClearDebugLogStatus(bool abIsChecked); - bool getClearDebugLogStatus(bool abDefaultValue); + bool getClearDebugLogStatus(bool abDefaultValue) const; private: explicit ServersSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp new file mode 100644 index 000000000..2d4f1c441 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.cpp @@ -0,0 +1,169 @@ +#include "settings_manager.h" + +SettingsManager::SettingsManager(const QString &_settingPath, + const QString &_defaultGroup, + const QString &_defaultSubGroup, + QObject *parent) + : QObject(parent), settingPath(_settingPath), defaultGroup(_defaultGroup), defaultSubGroup(_defaultSubGroup) +{ +} + +QSettings SettingsManager::getSettings() const +{ + return QSettings(settingPath, QSettings::IniFormat); +} + +void SettingsManager::setValue(const QVariant &value, const QString &name) +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + settings.setValue(name, value); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::setValue(const QVariant &value, + const QString &name, + const QString &group, + const QString &subGroup) +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + settings.setValue(name, value); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::deleteValue(const QString &name) +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + settings.remove(name); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } +} + +void SettingsManager::deleteValue(const QString &name, const QString &group, const QString &subGroup) +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + settings.remove(name); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } +} + +QVariant SettingsManager::getValue(const QString &name) const +{ + auto settings = getSettings(); + + if (!defaultGroup.isEmpty()) { + settings.beginGroup(defaultGroup); + } + + if (!defaultSubGroup.isEmpty()) { + settings.beginGroup(defaultSubGroup); + } + + QVariant value = settings.value(name); + + if (!defaultSubGroup.isEmpty()) { + settings.endGroup(); + } + + if (!defaultGroup.isEmpty()) { + settings.endGroup(); + } + + return value; +} + +QVariant SettingsManager::getValue(const QString &name, const QString &group, const QString &subGroup) const +{ + auto settings = getSettings(); + + if (!group.isEmpty()) { + settings.beginGroup(group); + } + + if (!subGroup.isEmpty()) { + settings.beginGroup(subGroup); + } + + QVariant value = settings.value(name); + + if (!subGroup.isEmpty()) { + settings.endGroup(); + } + + if (!group.isEmpty()) { + settings.endGroup(); + } + + return value; +} + +/** + * Calls sync on the underlying QSettings object + */ +void SettingsManager::sync() +{ + auto settings = getSettings(); + + settings.sync(); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/settings_manager.h b/libcockatrice_settings/libcockatrice/settings/settings_manager.h new file mode 100644 index 000000000..ad828f089 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/settings_manager.h @@ -0,0 +1,40 @@ +/** + * @file settings_manager.h + * @ingroup Settings + * @brief TODO: Document this. + */ + +#ifndef SETTINGSMANAGER_H +#define SETTINGSMANAGER_H + +#include +#include +#include + +class SettingsManager : public QObject +{ + Q_OBJECT +public: + explicit SettingsManager(const QString &settingPath, + const QString &defaultGroup = QString(), + const QString &defaultSubGroup = QString(), + QObject *parent = nullptr); + QVariant getValue(const QString &name) const; + QVariant getValue(const QString &name, const QString &group, const QString &subGroup = QString()) const; + void sync(); + +protected: + QString settingPath; + QString defaultGroup; + QString defaultSubGroup; + + QSettings getSettings() const; + + void setValue(const QVariant &value, const QString &name); + void + setValue(const QVariant &value, const QString &name, const QString &group, const QString &subGroup = QString()); + void deleteValue(const QString &name); + void deleteValue(const QString &name, const QString &group, const QString &subGroup = QString()); +}; + +#endif // SETTINGSMANAGER_H diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt new file mode 100644 index 000000000..c0c7d8cc9 --- /dev/null +++ b/libcockatrice_utility/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) +project(Utility VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +set(UTILITY_SOURCES libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp + libcockatrice/utility/passwordhasher.cpp +) + +set(UTILITY_HEADERS + libcockatrice/utility/color.h + libcockatrice/utility/expression.h + libcockatrice/utility/levenshtein.h + libcockatrice/utility/macros.h + libcockatrice/utility/passwordhasher.h + libcockatrice/utility/trice_limits.h + libcockatrice/utility/zone_names.h +) + +add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS}) + +target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE}) + +set(ORACLE_LIBS) + +include_directories(${${COCKATRICE_QT_VERSION_NAME}Core_INCLUDE_DIRS}) diff --git a/common/card_ref.h b/libcockatrice_utility/libcockatrice/utility/card_ref.h similarity index 87% rename from common/card_ref.h rename to libcockatrice_utility/libcockatrice/utility/card_ref.h index d29eee0bb..d89fe590b 100644 --- a/common/card_ref.h +++ b/libcockatrice_utility/libcockatrice/utility/card_ref.h @@ -19,6 +19,11 @@ struct CardRef { return name == other.name && providerId == other.providerId; } + + bool isEmpty() const + { + return name.isEmpty() && providerId.isEmpty(); + } }; #endif // CARD_REF_H diff --git a/libcockatrice_utility/libcockatrice/utility/color.h b/libcockatrice_utility/libcockatrice/utility/color.h new file mode 100644 index 000000000..f02df3a0e --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/color.h @@ -0,0 +1,100 @@ +#ifndef COLOR_H +#define COLOR_H + +#ifdef QT_GUI_LIB +#include +#endif + +#include + +#ifdef QT_GUI_LIB +inline QColor convertColorToQColor(const color &c) +{ + return QColor(c.r(), c.g(), c.b()); +} + +inline color convertQColorToColor(const QColor &c) +{ + color result; + result.set_r(c.red()); + result.set_g(c.green()); + result.set_b(c.blue()); + return result; +} + +#include + +namespace GameSpecificColors +{ +namespace MTG +{ +inline QColor colorHelper(const QString &name) +{ + static const QMap colorMap = { + {"W", QColor(245, 245, 220)}, + {"U", QColor(80, 140, 255)}, + {"B", QColor(60, 60, 60)}, + {"R", QColor(220, 60, 50)}, + {"G", QColor(70, 160, 70)}, + {"Creature", QColor(70, 130, 180)}, + {"Instant", QColor(138, 43, 226)}, + {"Sorcery", QColor(199, 21, 133)}, + {"Enchantment", QColor(218, 165, 32)}, + {"Artifact", QColor(169, 169, 169)}, + {"Planeswalker", QColor(210, 105, 30)}, + {"Land", QColor(110, 80, 50)}, + }; + + if (colorMap.contains(name)) + return colorMap[name]; + + if (name.length() == 1 && colorMap.contains(name.toUpper())) + return colorMap[name.toUpper()]; + + uint h = qHash(name); + int r = 100 + (h % 120); + int g = 100 + ((h >> 8) % 120); + int b = 100 + ((h >> 16) % 120); + + return QColor(r, g, b); +} + +inline QList> sortManaMapWUBRGCFirst(const QMap &input) +{ + static const QStringList priorityOrder = {"W", "U", "B", "R", "G", "C"}; + + QList> result; + QSet consumed; + + // 1. Add priority colors in fixed order + for (const QString &key : priorityOrder) { + auto it = input.find(key); + if (it != input.end()) { + result.append({it.key(), it.value()}); + consumed.insert(it.key()); + } + } + + // 2. Add remaining keys (QMap iteration is already sorted) + for (auto it = input.begin(); it != input.end(); ++it) { + if (!consumed.contains(it.key())) { + result.append({it.key(), it.value()}); + } + } + + return result; +} +} // namespace MTG +} // namespace GameSpecificColors +#endif + +inline color makeColor(int r, int g, int b) +{ + color result; + result.set_r(r); + result.set_g(g); + result.set_b(b); + return result; +} + +#endif diff --git a/common/expression.cpp b/libcockatrice_utility/libcockatrice/utility/expression.cpp similarity index 98% rename from common/expression.cpp rename to libcockatrice_utility/libcockatrice/utility/expression.cpp index 9cb40ea3c..42073670c 100644 --- a/common/expression.cpp +++ b/libcockatrice_utility/libcockatrice/utility/expression.cpp @@ -1,6 +1,6 @@ #include "expression.h" -#include "./lib/peglib.h" +#include "peglib.h" #include #include @@ -20,7 +20,7 @@ peg::parser math(R"( NUMBER <- < '-'? [0-9]+ > NAME <- < [a-z][a-z0-9]* > - VARIABLE <- < [x] > + VARIABLE <- < [xX] > FUNCTION <- NAME '(' EXPRESSION ( [,\n] EXPRESSION )* ')' %whitespace <- [ \t\r]* diff --git a/common/expression.h b/libcockatrice_utility/libcockatrice/utility/expression.h similarity index 100% rename from common/expression.h rename to libcockatrice_utility/libcockatrice/utility/expression.h diff --git a/cockatrice/src/utility/levenshtein.cpp b/libcockatrice_utility/libcockatrice/utility/levenshtein.cpp similarity index 100% rename from cockatrice/src/utility/levenshtein.cpp rename to libcockatrice_utility/libcockatrice/utility/levenshtein.cpp diff --git a/cockatrice/src/utility/levenshtein.h b/libcockatrice_utility/libcockatrice/utility/levenshtein.h similarity index 65% rename from cockatrice/src/utility/levenshtein.h rename to libcockatrice_utility/libcockatrice/utility/levenshtein.h index b9b05a13c..e83235470 100644 --- a/cockatrice/src/utility/levenshtein.h +++ b/libcockatrice_utility/libcockatrice/utility/levenshtein.h @@ -1,3 +1,9 @@ +/** + * @file levenshtein.h + * @ingroup Core + * @brief TODO: Document this. + */ + #ifndef LEVENSHTEIN_H #define LEVENSHTEIN_H diff --git a/cockatrice/src/utility/macros.h b/libcockatrice_utility/libcockatrice/utility/macros.h similarity index 100% rename from cockatrice/src/utility/macros.h rename to libcockatrice_utility/libcockatrice/utility/macros.h diff --git a/common/passwordhasher.cpp b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp similarity index 96% rename from common/passwordhasher.cpp rename to libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp index bc5f072e8..c40c5f94f 100644 --- a/common/passwordhasher.cpp +++ b/libcockatrice_utility/libcockatrice/utility/passwordhasher.cpp @@ -1,8 +1,7 @@ #include "passwordhasher.h" -#include "rng_sfmt.h" - #include +#include QString PasswordHasher::computeHash(const QString &password, const QString &salt) { diff --git a/common/passwordhasher.h b/libcockatrice_utility/libcockatrice/utility/passwordhasher.h similarity index 100% rename from common/passwordhasher.h rename to libcockatrice_utility/libcockatrice/utility/passwordhasher.h diff --git a/libcockatrice_utility/libcockatrice/utility/peglib.h b/libcockatrice_utility/libcockatrice/utility/peglib.h new file mode 100644 index 000000000..3ae6040c4 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/peglib.h @@ -0,0 +1,5758 @@ +// +// peglib.h +// +// Copyright (c) 2022 Yuji Hirose. All rights reserved. +// MIT License +// + +#pragma once + +/* + * Configuration + */ + +#ifndef CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT +#define CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT 32 +#endif + +#include +#include +#include +#include +#include +#if __has_include() +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(__cplusplus) || __cplusplus < 201703L +#error "Requires complete C++17 support" +#endif + +namespace peg { + +/*----------------------------------------------------------------------------- + * scope_exit + *---------------------------------------------------------------------------*/ + +// This is based on +// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". + +template struct scope_exit { + explicit scope_exit(EF &&f) + : exit_function(std::move(f)), execute_on_destruction{true} {} + + scope_exit(scope_exit &&rhs) + : exit_function(std::move(rhs.exit_function)), + execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } + + ~scope_exit() { + if (execute_on_destruction) { this->exit_function(); } + } + + void release() { this->execute_on_destruction = false; } + +private: + scope_exit(const scope_exit &) = delete; + void operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; + + EF exit_function; + bool execute_on_destruction; +}; + +/*----------------------------------------------------------------------------- + * UTF8 functions + *---------------------------------------------------------------------------*/ + +inline size_t codepoint_length(const char *s8, size_t l) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + return 1; + } else if ((b & 0xE0) == 0xC0 && l >= 2) { + return 2; + } else if ((b & 0xF0) == 0xE0 && l >= 3) { + return 3; + } else if ((b & 0xF8) == 0xF0 && l >= 4) { + return 4; + } + } + return 0; +} + +inline size_t codepoint_count(const char *s8, size_t l) { + size_t count = 0; + for (size_t i = 0; i < l;) { + auto len = codepoint_length(s8 + i, l - i); + if (len == 0) { + // Invalid UTF-8 byte, treat as single byte to avoid infinite loop + len = 1; + } + i += len; + count++; + } + return count; +} + +inline size_t encode_codepoint(char32_t cp, char *buff) { + if (cp < 0x0080) { + buff[0] = static_cast(cp & 0x7F); + return 1; + } else if (cp < 0x0800) { + buff[0] = static_cast(0xC0 | ((cp >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } else if (cp < 0xD800) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0xE000) { + // D800 - DFFF is invalid... + return 0; + } else if (cp < 0x10000) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0x110000) { + buff[0] = static_cast(0xF0 | ((cp >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; +} + +inline std::string encode_codepoint(char32_t cp) { + char buff[4]; + auto l = encode_codepoint(cp, buff); + return std::string(buff, l); +} + +inline bool decode_codepoint(const char *s8, size_t l, size_t &bytes, + char32_t &cp) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + bytes = 1; + cp = b; + return true; + } else if ((b & 0xE0) == 0xC0) { + if (l >= 2) { + bytes = 2; + cp = ((static_cast(s8[0] & 0x1F)) << 6) | + (static_cast(s8[1] & 0x3F)); + return true; + } + } else if ((b & 0xF0) == 0xE0) { + if (l >= 3) { + bytes = 3; + cp = ((static_cast(s8[0] & 0x0F)) << 12) | + ((static_cast(s8[1] & 0x3F)) << 6) | + (static_cast(s8[2] & 0x3F)); + return true; + } + } else if ((b & 0xF8) == 0xF0) { + if (l >= 4) { + bytes = 4; + cp = ((static_cast(s8[0] & 0x07)) << 18) | + ((static_cast(s8[1] & 0x3F)) << 12) | + ((static_cast(s8[2] & 0x3F)) << 6) | + (static_cast(s8[3] & 0x3F)); + return true; + } + } + } + return false; +} + +inline size_t decode_codepoint(const char *s8, size_t l, char32_t &cp) { + size_t bytes; + if (decode_codepoint(s8, l, bytes, cp)) { return bytes; } + return 0; +} + +inline char32_t decode_codepoint(const char *s8, size_t l) { + char32_t cp = 0; + decode_codepoint(s8, l, cp); + return cp; +} + +inline std::u32string decode(const char *s8, size_t l) { + std::u32string out; + size_t i = 0; + while (i < l) { + auto beg = i++; + while (i < l && (s8[i] & 0xc0) == 0x80) { + i++; + } + out += decode_codepoint(&s8[beg], (i - beg)); + } + return out; +} + +template const char *u8(const T *s) { + return reinterpret_cast(s); +} + +/*----------------------------------------------------------------------------- + * escape_characters + *---------------------------------------------------------------------------*/ + +inline std::string escape_characters(const char *s, size_t n) { + std::string str; + for (size_t i = 0; i < n; i++) { + auto c = s[i]; + switch (c) { + case '\f': str += "\\f"; break; + case '\n': str += "\\n"; break; + case '\r': str += "\\r"; break; + case '\t': str += "\\t"; break; + case '\v': str += "\\v"; break; + default: str += c; break; + } + } + return str; +} + +inline std::string escape_characters(std::string_view sv) { + return escape_characters(sv.data(), sv.size()); +} + +/*----------------------------------------------------------------------------- + * resolve_escape_sequence + *---------------------------------------------------------------------------*/ + +inline bool is_hex(char c, int &v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } + return false; +} + +inline bool is_digit(char c, int &v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } + return false; +} + +inline std::pair parse_hex_number(const char *s, size_t n, + size_t i) { + int ret = 0; + int val; + while (i < n && is_hex(s[i], val)) { + ret = static_cast(ret * 16 + val); + i++; + } + return std::pair(ret, i); +} + +inline std::pair parse_octal_number(const char *s, size_t n, + size_t i) { + int ret = 0; + int val; + while (i < n && is_digit(s[i], val)) { + ret = static_cast(ret * 8 + val); + i++; + } + return std::pair(ret, i); +} + +inline std::string resolve_escape_sequence(const char *s, size_t n) { + std::string r; + r.reserve(n); + + size_t i = 0; + while (i < n) { + auto ch = s[i]; + if (ch == '\\') { + i++; + assert(i < n); + + switch (s[i]) { + case 'f': + r += '\f'; + i++; + break; + case 'n': + r += '\n'; + i++; + break; + case 'r': + r += '\r'; + i++; + break; + case 't': + r += '\t'; + i++; + break; + case 'v': + r += '\v'; + i++; + break; + case '\'': + r += '\''; + i++; + break; + case '"': + r += '"'; + i++; + break; + case '[': + r += '['; + i++; + break; + case ']': + r += ']'; + i++; + break; + case '\\': + r += '\\'; + i++; + break; + case 'x': + case 'u': { + char32_t cp; + std::tie(cp, i) = parse_hex_number(s, n, i + 1); + r += encode_codepoint(cp); + break; + } + default: { + char32_t cp; + std::tie(cp, i) = parse_octal_number(s, n, i); + r += encode_codepoint(cp); + break; + } + } + } else { + r += ch; + i++; + } + } + return r; +} + +/*----------------------------------------------------------------------------- + * token_to_number_ - This function should be removed eventually + *---------------------------------------------------------------------------*/ + +template T token_to_number_(std::string_view sv) { + T n = 0; +#if __has_include() + if constexpr (!std::is_floating_point::value) { + std::from_chars(sv.data(), sv.data() + sv.size(), n); +#else + if constexpr (false) { +#endif + } else { + auto s = std::string(sv); + std::istringstream ss(s); + ss >> n; + } + return n; +} + +inline std::string to_lower(std::string s) { + for (auto &c : s) { + c = static_cast(std::tolower(static_cast(c))); + } + return s; +} + +/*----------------------------------------------------------------------------- + * Trie + *---------------------------------------------------------------------------*/ + +class Trie { +public: + Trie(const std::vector &items, bool ignore_case) + : ignore_case_(ignore_case), items_count_(items.size()) { + size_t id = 0; + for (const auto &item : items) { + const auto &s = ignore_case ? to_lower(item) : item; + if (item.size() > max_len_) { max_len_ = item.size(); } + for (size_t len = 1; len <= item.size(); len++) { + auto last = len == item.size(); + std::string_view sv(s.data(), len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + dic_.emplace(sv, Info{last, last, id}); + } else if (last) { + it->second.match = true; + } else { + it->second.done = false; + } + } + id++; + } + } + + size_t match(const char *text, size_t text_len, size_t &id) const { + auto limit = std::min(text_len, max_len_); + std::string lower_text; + if (ignore_case_) { + lower_text = to_lower(std::string(text, limit)); + text = lower_text.data(); + } + + size_t match_len = 0; + auto done = false; + size_t len = 1; + while (!done && len <= limit) { + std::string_view sv(text, len); + auto it = dic_.find(sv); + if (it == dic_.end()) { + done = true; + } else { + if (it->second.match) { + match_len = len; + id = it->second.id; + } + if (it->second.done) { done = true; } + } + len += 1; + } + return match_len; + } + + size_t size() const { return dic_.size(); } + size_t items_count() const { return items_count_; } + + friend struct ComputeFirstSet; + +private: + struct Info { + bool done; + bool match; + size_t id; + }; + + // TODO: Use unordered_map when heterogeneous lookup is supported in C++20 + // std::unordered_map dic_; + std::map> dic_; + + bool ignore_case_; + size_t items_count_; + size_t max_len_ = 0; +}; + +/*----------------------------------------------------------------------------- + * PEG + *---------------------------------------------------------------------------*/ + +/* + * Line information utility function + */ +inline std::pair line_info(const char *start, const char *cur) { + auto p = start; + auto col_ptr = p; + auto no = 1; + + while (p < cur) { + if (*p == '\n') { + no++; + col_ptr = p + 1; + } + p++; + } + + auto col = codepoint_count(col_ptr, p - col_ptr) + 1; + + return std::pair(no, col); +} + +/* + * String tag + */ +inline constexpr unsigned int str2tag_core(const char *s, size_t l, + unsigned int h) { + return (l == 0) ? h + : str2tag_core(s + 1, l - 1, + (h * 33) ^ static_cast(*s)); +} + +inline constexpr unsigned int str2tag(std::string_view sv) { + return str2tag_core(sv.data(), sv.size(), 0); +} + +namespace udl { + +inline constexpr unsigned int operator""_(const char *s, size_t l) { + return str2tag_core(s, l, 0); +} + +} // namespace udl + +/* + * Semantic values + */ +class Context; + +struct SemanticValues : protected std::vector { + SemanticValues() = default; + SemanticValues(Context *c) : c_(c) {} + + // Input text + const char *path = nullptr; + const char *ss = nullptr; + + // Matched string + std::string_view sv() const { return sv_; } + + // Definition name + const std::string &name() const { return name_; } + + std::vector tags; + + // Line number and column at which the matched string is + std::pair line_info() const; + + // Choice count + size_t choice_count() const { return choice_count_; } + + // Choice number (0 based index) + size_t choice() const { return choice_; } + + // Tokens + std::vector tokens; + + std::string_view token(size_t id = 0) const { + if (tokens.empty()) { return sv_; } + assert(id < tokens.size()); + return tokens[id]; + } + + // Token conversion + std::string token_to_string(size_t id = 0) const { + return std::string(token(id)); + } + + template T token_to_number() const { + return token_to_number_(token()); + } + + // Transform the semantic value vector to another vector + template + std::vector transform(size_t beg = 0, + size_t end = static_cast(-1)) const { + std::vector r; + end = (std::min)(end, size()); + for (size_t i = beg; i < end; i++) { + r.emplace_back(std::any_cast((*this)[i])); + } + return r; + } + + using std::vector::iterator; + using std::vector::const_iterator; + using std::vector::size; + using std::vector::empty; + using std::vector::assign; + using std::vector::begin; + using std::vector::end; + using std::vector::rbegin; + using std::vector::rend; + using std::vector::operator[]; + using std::vector::at; + using std::vector::resize; + using std::vector::front; + using std::vector::back; + using std::vector::push_back; + using std::vector::pop_back; + using std::vector::insert; + using std::vector::erase; + using std::vector::clear; + using std::vector::swap; + using std::vector::emplace; + using std::vector::emplace_back; + +private: + friend class Context; + friend class Dictionary; + friend class Sequence; + friend class PrioritizedChoice; + friend class Repetition; + friend class Holder; + friend class PrecedenceClimbing; + + Context *c_ = nullptr; + std::string_view sv_; + size_t choice_count_ = 0; + size_t choice_ = 0; + std::string name_; +}; + +/* + * Semantic action + */ +template std::any call(F fn, Args &&...args) { + using R = decltype(fn(std::forward(args)...)); + if constexpr (std::is_void::value) { + fn(std::forward(args)...); + return std::any(); + } else if constexpr (std::is_same::type, + std::any>::value) { + return fn(std::forward(args)...); + } else { + return std::any(fn(std::forward(args)...)); + } +} + +template +struct argument_count : argument_count {}; +template +struct argument_count + : std::integral_constant {}; +template +struct argument_count + : std::integral_constant {}; +template +struct argument_count + : std::integral_constant {}; + +class Action { +public: + Action() = default; + Action(Action &&rhs) = default; + template Action(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Action &operator=(const Action &rhs) = default; + + operator bool() const { return bool(fn_); } + + std::any operator()(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + return fn_(vs, dt, predicate_data); + } + +private: + using Fty = std::function; + + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 1) { + return [fn](auto &vs, auto & /*dt*/, const auto & /*predicate_data*/) { + return call(fn, vs); + }; + } else if constexpr (argument_count::value == 2) { + return [fn](auto &vs, auto &dt, const auto & /*predicate_data*/) { + return call(fn, vs, dt); + }; + } else { + return [fn](auto &vs, auto &dt, const auto &predicate_data) { + return call(fn, vs, dt, predicate_data); + }; + } + } + + Fty fn_; +}; + +class Predicate { +public: + Predicate() = default; + Predicate(Predicate &&rhs) = default; + template Predicate(F fn) : fn_(make_adaptor(fn)) {} + template void operator=(F fn) { fn_ = make_adaptor(fn); } + Predicate &operator=(const Predicate &rhs) = default; + + operator bool() const { return bool(fn_); } + + bool operator()(const SemanticValues &vs, const std::any &dt, + std::string &msg, std::any &predicate_data) const { + return fn_(vs, dt, msg, predicate_data); + } + +private: + using Fty = std::function; + + template Fty make_adaptor(F fn) { + if constexpr (argument_count::value == 3) { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto & /*predicate_data*/) { return fn(vs, dt, msg); }; + } else { + return [fn](const auto &vs, const auto &dt, auto &msg, + auto &predicate_data) { + return fn(vs, dt, msg, predicate_data); + }; + } + } + + Fty fn_; +}; + +/* + * Parse result helper + */ +inline bool success(size_t len) { return len != static_cast(-1); } + +inline bool fail(size_t len) { return len == static_cast(-1); } + +/* + * Log + */ +using Log = std::function; + +/* + * ErrorInfo + */ +class Definition; + +struct ErrorInfo { + const char *error_pos = nullptr; + std::vector> expected_tokens; + const char *message_pos = nullptr; + std::string message; + std::string label; + const char *last_output_pos = nullptr; + bool keep_previous_token = false; + + void clear() { + error_pos = nullptr; + expected_tokens.clear(); + message_pos = nullptr; + message.clear(); + } + + void add(const char *error_literal, const Definition *error_rule) { + for (const auto &[t, r] : expected_tokens) { + if (t == error_literal && r == error_rule) { return; } + } + expected_tokens.emplace_back(error_literal, error_rule); + } + + void output_log(const Log &log, const char *s, size_t n); + +private: + int cast_char(char c) const { return static_cast(c); } + + std::string heuristic_error_token(const char *s, size_t n, + const char *pos) const { + auto len = n - std::distance(s, pos); + if (len) { + size_t i = 0; + auto c = cast_char(pos[i++]); + if (!std::ispunct(c) && !std::isspace(c)) { + while (i < len && !std::ispunct(cast_char(pos[i])) && + !std::isspace(cast_char(pos[i]))) { + i++; + } + } + + size_t count = CPPPEGLIB_HEURISTIC_ERROR_TOKEN_MAX_CHAR_COUNT; + size_t j = 0; + while (count > 0 && j < i) { + j += codepoint_length(&pos[j], i - j); + count--; + } + + return escape_characters(pos, j); + } + return std::string(); + } + + std::string replace_all(std::string str, const std::string &from, + const std::string &to) const { + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } + return str; + } +}; + +/* + * Context + */ +class Ope; + +using TracerEnter = std::function; + +using TracerLeave = std::function; + +using TracerStartOrEnd = std::function; + +class Context { +public: + const char *path; + const char *s; + const size_t l; + + ErrorInfo error_info; + bool recovered = false; + + std::vector> value_stack; + size_t value_stack_size = 0; + + std::vector rule_stack; + std::vector>> args_stack; + + size_t in_token_boundary_count = 0; + + std::shared_ptr whitespaceOpe; + bool in_whitespace = false; + + std::shared_ptr wordOpe; + + std::vector> capture_entries; + + std::vector cut_stack; + + const size_t def_count; + const bool enablePackratParsing; + std::vector cache_registered; + std::vector cache_success; + + std::map, std::tuple> + cache_values; + + // Left recursion support + struct LRMemo { + size_t len = static_cast(-1); + std::any val; + }; + std::map, LRMemo> lr_memo; + + // Rules whose lr_memo was hit during the current parse scope. + // Used to track LR cycle membership. + std::set lr_refs_hit; + + // Rules currently in their seeding/growing phase at a given position. + // Protected from having their lr_memo erased by inner growers. + std::set> lr_active_seeds; + + void clear_packrat_cache(const char *pos, size_t def_id) { + if (!enablePackratParsing) { return; } + auto col = static_cast(pos - s); + auto idx = def_count * col + def_id; + if (idx < cache_registered.size()) { + cache_registered[idx] = false; + cache_success[idx] = false; + } + cache_values.erase(std::make_pair(col, def_id)); + } + + void write_packrat_cache(const char *pos, size_t def_id, size_t len, + const std::any &val) { + if (!enablePackratParsing) { return; } + auto col = pos - s; + auto idx = def_count * static_cast(col) + def_id; + if (idx >= cache_registered.size()) { return; } + cache_registered[idx] = true; + cache_success[idx] = true; + auto key = std::pair(col, def_id); + cache_values[key] = std::pair(len, val); + } + + TracerEnter tracer_enter; + TracerLeave tracer_leave; + std::any trace_data; + const bool verbose_trace; + + Log log; + + Context(const char *path, const char *s, size_t l, size_t def_count, + std::shared_ptr whitespaceOpe, std::shared_ptr wordOpe, + bool enablePackratParsing, TracerEnter tracer_enter, + TracerLeave tracer_leave, std::any trace_data, bool verbose_trace, + Log log) + : path(path), s(s), l(l), whitespaceOpe(whitespaceOpe), wordOpe(wordOpe), + def_count(def_count), enablePackratParsing(enablePackratParsing), + cache_registered(enablePackratParsing ? def_count * (l + 1) : 0), + cache_success(enablePackratParsing ? def_count * (l + 1) : 0), + tracer_enter(tracer_enter), tracer_leave(tracer_leave), + trace_data(trace_data), verbose_trace(verbose_trace), log(log) { + + push_args({}); + } + + ~Context() { + assert(!value_stack_size); + assert(cut_stack.empty()); + } + + Context(const Context &) = delete; + Context(Context &&) = delete; + Context operator=(const Context &) = delete; + + // Per-rule packrat stats (populated when packrat_stats is non-null) + struct PackratStats { + size_t hits = 0; + size_t misses = 0; + }; + std::vector *packrat_stats = nullptr; + + // Per-rule packrat filter: if set, only rules with filter[def_id]=true + // use full memoization (cache_values map). Others use bitvector-only + // re-entry guard. + const std::vector *packrat_rule_filter = nullptr; + + template + void packrat(const char *a_s, size_t def_id, size_t &len, std::any &val, + T fn) { + if (!enablePackratParsing) { + fn(val); + return; + } + + auto col = a_s - s; + auto idx = def_count * static_cast(col) + def_id; + + if (cache_registered[idx]) { + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].hits++; + } + if (cache_success[idx]) { + auto key = std::pair(col, def_id); + std::tie(len, val) = cache_values[key]; + return; + } else { + len = static_cast(-1); + return; + } + } else { + // Pre-register as failure (re-entry guard for all rules) + cache_registered[idx] = true; + cache_success[idx] = false; + + if (packrat_stats && def_id < packrat_stats->size()) { + (*packrat_stats)[def_id].misses++; + } + + fn(val); + + bool full_memo = + !packrat_rule_filter || (def_id < packrat_rule_filter->size() && + (*packrat_rule_filter)[def_id]); + if (full_memo) { + if (success(len)) { write_packrat_cache(a_s, def_id, len, val); } + } else { + // Guard-only: undo registration so future calls re-parse + cache_registered[idx] = false; + } + return; + } + } + + // Semantic values + SemanticValues &push_semantic_values_scope() { + assert(value_stack_size <= value_stack.size()); + if (value_stack_size == value_stack.size()) { + value_stack.emplace_back(std::make_shared(this)); + } else { + auto &vs = *value_stack[value_stack_size]; + if (!vs.empty()) { + vs.clear(); + if (!vs.tags.empty()) { vs.tags.clear(); } + } + vs.sv_ = std::string_view(); + vs.choice_count_ = 0; + vs.choice_ = 0; + if (!vs.tokens.empty()) { vs.tokens.clear(); } + } + + auto &vs = *value_stack[value_stack_size++]; + vs.path = path; + vs.ss = s; + return vs; + } + + void pop_semantic_values_scope() { value_stack_size--; } + + // Arguments + void push_args(std::vector> &&args) { + args_stack.emplace_back(std::move(args)); + } + + void pop_args() { args_stack.pop_back(); } + + const std::vector> &top_args() const { + return args_stack[args_stack.size() - 1]; + } + + // Snapshot/Rollback + struct Snapshot { + size_t sv_size; + size_t sv_tags_size; + size_t sv_tokens_size; + std::string_view sv_sv; + size_t choice_count; + size_t choice; + size_t capture_size; + }; + + Snapshot snapshot(const SemanticValues &vs) const { + return {vs.size(), vs.tags.size(), vs.tokens.size(), vs.sv_, + vs.choice_count_, vs.choice_, capture_entries.size()}; + } + + void rollback(SemanticValues &vs, const Snapshot &snap) { + vs.resize(snap.sv_size); + vs.tags.resize(snap.sv_tags_size); + vs.tokens.resize(snap.sv_tokens_size); + vs.sv_ = snap.sv_sv; + vs.choice_count_ = snap.choice_count; + vs.choice_ = snap.choice; + capture_entries.resize(snap.capture_size); + } + + // Skip trailing whitespace with trace suppression. + // Returns whitespace length, or -1 on failure. + // No-op (returns 0) if inside a token boundary or no whitespaceOpe. + size_t skip_whitespace(const char *a_s, size_t n, SemanticValues &vs, + std::any &dt); + + // Error + void set_error_pos(const char *a_s, const char *literal = nullptr); + + // Trace + void trace_enter(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt); + void trace_leave(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt, size_t len); + bool is_traceable(const Ope &ope) const; + + // Line info + std::pair line_info(const char *cur) const { + std::call_once(source_line_index_init_, [this]() { + for (size_t pos = 0; pos < l; pos++) { + if (s[pos] == '\n') { source_line_index.push_back(pos); } + } + source_line_index.push_back(l); + }); + + auto pos = static_cast(std::distance(s, cur)); + + auto it = std::lower_bound( + source_line_index.begin(), source_line_index.end(), pos, + [](size_t element, size_t value) { return element < value; }); + + auto id = static_cast(std::distance(source_line_index.begin(), it)); + auto off = pos - (id == 0 ? 0 : source_line_index[id - 1] + 1); + return std::pair(id + 1, off + 1); + } + + size_t next_trace_id = 0; + std::vector trace_ids; + bool ignore_trace_state = false; + mutable std::once_flag source_line_index_init_; + mutable std::vector source_line_index; +}; + +/* + * Parser operators + */ +class Ope { +public: + struct Visitor; + + virtual ~Ope() = default; + size_t parse(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const; + virtual size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const = 0; + virtual void accept(Visitor &v) = 0; + + bool is_token_boundary = false; + bool is_choice_like = false; +}; + +// Keyword-guarded identifier data, heap-allocated only for matching Sequences. +// Avoids bloating all Sequence objects with bitsets and keyword sets. +struct KeywordGuardData { + std::bitset<256> identifier_first; // first char of identifier + std::bitset<256> identifier_rest; // subsequent chars of identifier + std::vector exact_keywords; // single-word keywords (lowercase) + std::vector prefix_keywords; // first word of compound keywords + size_t min_keyword_len = 0; + size_t max_keyword_len = 0; + + static bool matches_any(const std::vector &keywords, + std::string_view input) { + return std::any_of(keywords.begin(), keywords.end(), + [&](const auto &kw) { return kw == input; }); + } +}; + +class Sequence : public Ope { +public: + template + Sequence(const Args &...args) + : opes_{static_cast>(args)...} {} + Sequence(const std::vector> &opes) : opes_(opes) {} + Sequence(std::vector> &&opes) : opes_(std::move(opes)) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // Keyword-guarded identifier fast path: + // Fuses !ReservedKeyword into scan-then-lookup + if (kw_guard_) { + if (auto result = parse_keyword_guarded(s, n, vs, c, dt)) { + return *result; + } + // nullopt means prefix keyword match — fall through to normal path + } + size_t i = 0; + for (const auto &ope : opes_) { + auto len = ope->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + } + return i; + } + + void accept(Visitor &v) override; + + std::vector> opes_; + +private: + friend struct SetupFirstSets; + std::unique_ptr kw_guard_; + + // Returns parse result, or nullopt to fall through to normal path + std::optional parse_keyword_guarded(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + const auto &kw = *kw_guard_; + if (n < 1 || !kw.identifier_first.test(static_cast(*s))) { + c.set_error_pos(s); + return static_cast(-1); + } + // Scan identifier using bitset + size_t id_len = 1; + while (id_len < n && + kw.identifier_rest.test(static_cast(s[id_len]))) { + id_len++; + } + // Skip keyword matching if identifier length is out of range + if (id_len >= kw.min_keyword_len && id_len <= kw.max_keyword_len) { + char lower_buf[64]; + std::unique_ptr lower_heap; + char *lower = lower_buf; + if (id_len > sizeof(lower_buf)) { + lower_heap.reset(new char[id_len]); + lower = lower_heap.get(); + } + std::transform(s, s + id_len, lower, [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + std::string_view lower_sv(lower, id_len); + + if (KeywordGuardData::matches_any(kw.exact_keywords, lower_sv)) { + c.set_error_pos(s); + return static_cast(-1); + } + if (KeywordGuardData::matches_any(kw.prefix_keywords, lower_sv)) { + return std::nullopt; + } + } + // Success: emit token and consume trailing whitespace + vs.tokens.emplace_back(std::string_view(s, id_len)); + auto wl = c.skip_whitespace(s + id_len, n - id_len, vs, dt); + if (fail(wl)) { return wl; } + return id_len + wl; + } +}; + +struct FirstSet { + // First-Set: set of possible first bytes for an expression. + // Used by PrioritizedChoice to skip alternatives that cannot match. + std::bitset<256> chars; // byte values that can appear as the first byte + bool can_be_empty = false; // true if the expression can match empty string + bool any_char = false; // true if any character can appear (cannot filter) + const char *first_literal = nullptr; // first literal for error reporting + const Definition *first_rule = + nullptr; // first token rule for error reporting + + void merge(const FirstSet &other) { + chars |= other.chars; + if (other.can_be_empty) { can_be_empty = true; } + if (other.any_char) { any_char = true; } + // Note: first_literal/first_rule are NOT merged — per-alternative + } +}; + +class PrioritizedChoice : public Ope { +public: + template + PrioritizedChoice(bool for_label, const Args &...args) + : opes_{static_cast>(args)...}, + for_label_(for_label) { + is_choice_like = true; + } + PrioritizedChoice(const std::vector> &opes) + : opes_(opes) { + is_choice_like = true; + } + PrioritizedChoice(std::vector> &&opes) + : opes_(std::move(opes)) { + is_choice_like = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + size_t len = static_cast(-1); + + if (!for_label_) { c.cut_stack.push_back(false); } + auto se = scope_exit([&]() { + if (!for_label_) { c.cut_stack.pop_back(); } + }); + + size_t id = 0; + for (const auto &ope : opes_) { + // First-Set filtering: skip if next byte cannot start this alternative + if (n > 0 && id < first_sets_.size()) { + const auto &fs = first_sets_[id]; + if (!fs.any_char && !fs.can_be_empty && + !fs.chars.test(static_cast(*s))) { + if (c.log && (fs.first_literal || fs.first_rule)) { + if (c.error_info.error_pos <= s) { + if (c.error_info.error_pos < s || !(id > 0)) { + c.error_info.error_pos = s; + c.error_info.expected_tokens.clear(); + } + if (fs.first_literal) { + c.error_info.add(fs.first_literal, nullptr); + } else { + c.error_info.add(nullptr, fs.first_rule); + } + } + } + id++; + continue; + } + } + + if (!c.cut_stack.empty()) { c.cut_stack.back() = false; } + + auto snap = c.snapshot(vs); + c.error_info.keep_previous_token = id > 0; + + len = ope->parse(s, n, vs, c, dt); + + if (success(len)) { + vs.choice_count_ = opes_.size(); + vs.choice_ = id; + break; + } + + c.rollback(vs, snap); + + if (!c.cut_stack.empty() && c.cut_stack.back()) { break; } + + id++; + } + + c.error_info.keep_previous_token = false; + return len; + } + + void accept(Visitor &v) override; + + size_t size() const { return opes_.size(); } + + std::vector> opes_; + bool for_label_ = false; + std::vector first_sets_; +}; + +class Repetition : public Ope { +public: + Repetition(const std::shared_ptr &ope, size_t min, size_t max) + : ope_(ope), min_(min), max_(max) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + // ISpan fast path: tight loop for ASCII CharacterClass repetition. + // Safe because each ASCII match is exactly 1 byte, so byte count == match + // count. + if (span_bitset_) { + const auto &bitset = *span_bitset_; + size_t i = 0; + if (max_ == std::numeric_limits::max()) { + // Unbounded repetition (*, +): no per-iteration max check + while (i < n && bitset.test(static_cast(s[i]))) { + i++; + } + } else { + auto limit = std::min(n, max_); + while (i < limit && bitset.test(static_cast(s[i]))) { + i++; + } + } + if (i < min_) { + c.set_error_pos(s + i); + return static_cast(-1); + } + return i; + } + + size_t count = 0; + size_t i = 0; + while (count < min_) { + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { return len; } + i += len; + count++; + } + + while (count < max_) { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s + i, n - i, vs, c, dt); + if (fail(len)) { + c.rollback(vs, snap); + break; + } + i += len; + count++; + } + return i; + } + + void accept(Visitor &v) override; + + bool is_zom() const { + return min_ == 0 && max_ == std::numeric_limits::max(); + } + + static std::shared_ptr zom(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, + std::numeric_limits::max()); + } + + static std::shared_ptr oom(const std::shared_ptr &ope) { + return std::make_shared(ope, 1, + std::numeric_limits::max()); + } + + static std::shared_ptr opt(const std::shared_ptr &ope) { + return std::make_shared(ope, 0, 1); + } + + std::shared_ptr ope_; + size_t min_; + size_t max_; + const std::bitset<256> *span_bitset_ = + nullptr; // non-owning, set by SetupFirstSets +}; + +class AndPredicate : public Ope { +public: + AndPredicate(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + return 0; + } else { + return len; + } + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class NotPredicate : public Ope { +public: + NotPredicate(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto snap = c.snapshot(vs); + auto len = ope_->parse(s, n, vs, c, dt); + c.rollback(vs, snap); // Always rollback — predicates consume nothing + if (success(len)) { + c.set_error_pos(s); + return static_cast(-1); + } else { + return 0; + } + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Dictionary : public Ope, public std::enable_shared_from_this { +public: + Dictionary(const std::vector &v, bool ignore_case) + : trie_(v, ignore_case) { + is_choice_like = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + Trie trie_; +}; + +class LiteralString : public Ope, + public std::enable_shared_from_this { +public: + LiteralString(std::string &&s, bool ignore_case) + : lit_(std::move(s)), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} + + LiteralString(const std::string &s, bool ignore_case) + : lit_(s), ignore_case_(ignore_case), + lower_lit_(ignore_case ? to_lower(lit_) : std::string()), + is_word_(false) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::string lit_; + bool ignore_case_; + std::string lower_lit_; // pre-computed for ignore_case + mutable std::once_flag init_is_word_; + mutable bool is_word_; +}; + +class CharacterClass : public Ope, + public std::enable_shared_from_this { +public: + CharacterClass(const std::string &s, bool negated, bool ignore_case) + : negated_(negated), ignore_case_(ignore_case) { + auto chars = decode(s.data(), s.length()); + auto i = 0u; + while (i < chars.size()) { + if (i + 2 < chars.size() && chars[i + 1] == '-') { + auto cp1 = chars[i]; + auto cp2 = chars[i + 2]; + ranges_.emplace_back(std::pair(cp1, cp2)); + i += 3; + } else { + auto cp = chars[i]; + ranges_.emplace_back(std::pair(cp, cp)); + i += 1; + } + } + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + CharacterClass(const std::vector> &ranges, + bool negated, bool ignore_case) + : ranges_(ranges), negated_(negated), ignore_case_(ignore_case) { + assert(!ranges_.empty()); + setup_ascii_bitset(); + } + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); + + for (const auto &range : ranges_) { + if (in_range(range, cp)) { + if (negated_) { + c.set_error_pos(s); + return static_cast(-1); + } else { + return len; + } + } + } + + if (negated_) { + return len; + } else { + c.set_error_pos(s); + return static_cast(-1); + } + } + + void accept(Visitor &v) override; + + friend struct ComputeFirstSet; + + bool is_ascii_only() const { return is_ascii_only_; } + const std::bitset<256> &ascii_bitset() const { return ascii_bitset_; } + +private: + bool in_range(const std::pair &range, char32_t cp) const { + if (ignore_case_) { + auto cpl = std::tolower(cp); + return std::tolower(range.first) <= cpl && + cpl <= std::tolower(range.second); + } else { + return range.first <= cp && cp <= range.second; + } + } + + void setup_ascii_bitset() { + if (negated_) { return; } // negated classes can match non-ASCII + for (const auto &[lo, hi] : ranges_) { + if (lo > 0x7F || hi > 0x7F) { return; } + } + is_ascii_only_ = true; + for (const auto &[lo, hi] : ranges_) { + for (auto cp = lo; cp <= hi; cp++) { + auto ch = static_cast(cp); + ascii_bitset_.set(ch); + if (ignore_case_) { + ascii_bitset_.set(static_cast(std::toupper(ch))); + ascii_bitset_.set(static_cast(std::tolower(ch))); + } + } + } + } + + std::vector> ranges_; + bool negated_; + bool ignore_case_; + std::bitset<256> ascii_bitset_; + bool is_ascii_only_ = false; +}; + +class Character : public Ope, public std::enable_shared_from_this { +public: + Character(char32_t ch) : ch_(ch) {} + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + + char32_t cp = 0; + auto len = decode_codepoint(s, n, cp); + + if (cp != ch_) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor &v) override; + + char32_t ch_; +}; + +class AnyCharacter : public Ope, + public std::enable_shared_from_this { +public: + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + auto len = codepoint_length(s, n); + if (len < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor &v) override; +}; + +class CaptureScope : public Ope { +public: + CaptureScope(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto cap_snap = c.capture_entries.size(); + auto len = ope_->parse(s, n, vs, c, dt); + c.capture_entries.resize(cap_snap); // Always rollback (isolation) + return len; + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Capture : public Ope { +public: + using MatchAction = std::function; + + Capture(const std::shared_ptr &ope, MatchAction ma) + : ope_(ope), match_action_(ma) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto len = ope_->parse(s, n, vs, c, dt); + if (success(len) && match_action_) { match_action_(s, len, c); } + return len; + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; + MatchAction match_action_; +}; + +class TokenBoundary : public Ope { +public: + TokenBoundary(const std::shared_ptr &ope) : ope_(ope) { + is_token_boundary = true; + } + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Ignore : public Ope { +public: + Ignore(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues & /*vs*/, + Context &c, std::any &dt) const override { + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { c.pop_semantic_values_scope(); }); + return ope_->parse(s, n, chvs, c, dt); + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +using Parser = std::function; + +class User : public Ope { +public: + User(Parser fn) : fn_(fn) {} + size_t parse_core(const char *s, size_t n, SemanticValues &vs, + Context & /*c*/, std::any &dt) const override { + assert(fn_); + return fn_(s, n, vs, dt); + } + void accept(Visitor &v) override; + std::function + fn_; +}; + +class WeakHolder : public Ope { +public: + WeakHolder(const std::shared_ptr &ope) : weak_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + auto ope = weak_.lock(); + assert(ope); + return ope->parse(s, n, vs, c, dt); + } + + void accept(Visitor &v) override; + + std::weak_ptr weak_; +}; + +class Holder : public Ope { +public: + Holder(Definition *outer) : outer_(outer) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::any reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const; + + const std::string &name() const; + const std::string &trace_name() const; + + std::shared_ptr ope_; + Definition *outer_; + mutable std::once_flag trace_name_init_; + mutable std::string trace_name_; + + friend class Definition; +}; + +using Grammar = std::unordered_map; + +class Reference : public Ope, public std::enable_shared_from_this { +public: + Reference(const Grammar &grammar, const std::string &name, const char *s, + bool is_macro, const std::vector> &args) + : grammar_(grammar), name_(name), s_(s), is_macro_(is_macro), args_(args), + rule_(nullptr), iarg_(0) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr get_core_operator() const; + + const Grammar &grammar_; + const std::string name_; + const char *s_; + + const bool is_macro_; + const std::vector> args_; + + Definition *rule_; + size_t iarg_; +}; + +class Whitespace : public Ope { +public: + Whitespace(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + if (c.in_whitespace) { return 0; } + c.in_whitespace = true; + auto se = scope_exit([&]() { c.in_whitespace = false; }); + return ope_->parse(s, n, vs, c, dt); + } + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class BackReference : public Ope { +public: + BackReference(std::string &&name) : name_(std::move(name)) {} + + BackReference(const std::string &name) : name_(name) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::string name_; +}; + +class PrecedenceClimbing : public Ope { +public: + using BinOpeInfo = std::map>; + + PrecedenceClimbing(const std::shared_ptr &atom, + const std::shared_ptr &binop, const BinOpeInfo &info, + const Definition &rule) + : atom_(atom), binop_(binop), info_(info), rule_(rule) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override { + return parse_expression(s, n, vs, c, dt, 0); + } + + void accept(Visitor &v) override; + + std::shared_ptr atom_; + std::shared_ptr binop_; + BinOpeInfo info_; + const Definition &rule_; + +private: + size_t parse_expression(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt, size_t min_prec) const; + + Definition &get_reference_for_binop(Context &c) const; +}; + +class Recovery : public Ope { +public: + Recovery(const std::shared_ptr &ope) : ope_(ope) {} + + size_t parse_core(const char *s, size_t n, SemanticValues &vs, Context &c, + std::any &dt) const override; + + void accept(Visitor &v) override; + + std::shared_ptr ope_; +}; + +class Cut : public Ope, public std::enable_shared_from_this { +public: + size_t parse_core(const char * /*s*/, size_t /*n*/, SemanticValues & /*vs*/, + Context &c, std::any & /*dt*/) const override { + if (!c.cut_stack.empty()) { c.cut_stack.back() = true; } + return 0; + } + + void accept(Visitor &v) override; +}; + +/* + * Factories + */ +template std::shared_ptr seq(Args &&...args) { + return std::make_shared(static_cast>(args)...); +} + +template std::shared_ptr cho(Args &&...args) { + return std::make_shared( + false, static_cast>(args)...); +} + +template std::shared_ptr cho4label_(Args &&...args) { + return std::make_shared( + true, static_cast>(args)...); +} + +inline std::shared_ptr zom(const std::shared_ptr &ope) { + return Repetition::zom(ope); +} + +inline std::shared_ptr oom(const std::shared_ptr &ope) { + return Repetition::oom(ope); +} + +inline std::shared_ptr opt(const std::shared_ptr &ope) { + return Repetition::opt(ope); +} + +inline std::shared_ptr rep(const std::shared_ptr &ope, size_t min, + size_t max) { + return std::make_shared(ope, min, max); +} + +inline std::shared_ptr apd(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr npd(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr dic(const std::vector &v, + bool ignore_case) { + return std::make_shared(v, ignore_case); +} + +inline std::shared_ptr lit(std::string &&s) { + return std::make_shared(s, false); +} + +inline std::shared_ptr liti(std::string &&s) { + return std::make_shared(s, true); +} + +inline std::shared_ptr cls(const std::string &s) { + return std::make_shared(s, false, false); +} + +inline std::shared_ptr +cls(const std::vector> &ranges, + bool ignore_case = false) { + return std::make_shared(ranges, false, ignore_case); +} + +inline std::shared_ptr ncls(const std::string &s) { + return std::make_shared(s, true, false); +} + +inline std::shared_ptr +ncls(const std::vector> &ranges, + bool ignore_case = false) { + return std::make_shared(ranges, true, ignore_case); +} + +inline std::shared_ptr chr(char32_t dt) { + return std::make_shared(dt); +} + +inline std::shared_ptr dot() { return std::make_shared(); } + +inline std::shared_ptr csc(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr cap(const std::shared_ptr &ope, + Capture::MatchAction ma) { + return std::make_shared(ope, ma); +} + +inline std::shared_ptr tok(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr ign(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr +usr(std::function + fn) { + return std::make_shared(fn); +} + +inline std::shared_ptr ref(const Grammar &grammar, const std::string &name, + const char *s, bool is_macro, + const std::vector> &args) { + return std::make_shared(grammar, name, s, is_macro, args); +} + +inline std::shared_ptr wsp(const std::shared_ptr &ope) { + return std::make_shared(std::make_shared(ope)); +} + +inline std::shared_ptr bkr(std::string &&name) { + return std::make_shared(name); +} + +inline std::shared_ptr pre(const std::shared_ptr &atom, + const std::shared_ptr &binop, + const PrecedenceClimbing::BinOpeInfo &info, + const Definition &rule) { + return std::make_shared(atom, binop, info, rule); +} + +inline std::shared_ptr rec(const std::shared_ptr &ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr cut() { return std::make_shared(); } + +/* + * Visitor + */ +struct Ope::Visitor { + virtual ~Visitor() {} + virtual void visit(Sequence &) {} + virtual void visit(PrioritizedChoice &) {} + virtual void visit(Repetition &) {} + virtual void visit(AndPredicate &) {} + virtual void visit(NotPredicate &) {} + virtual void visit(Dictionary &) {} + virtual void visit(LiteralString &) {} + virtual void visit(CharacterClass &) {} + virtual void visit(Character &) {} + virtual void visit(AnyCharacter &) {} + virtual void visit(CaptureScope &) {} + virtual void visit(Capture &) {} + virtual void visit(TokenBoundary &) {} + virtual void visit(Ignore &) {} + virtual void visit(User &) {} + virtual void visit(WeakHolder &) {} + virtual void visit(Holder &) {} + virtual void visit(Reference &) {} + virtual void visit(Whitespace &) {} + virtual void visit(BackReference &) {} + virtual void visit(PrecedenceClimbing &) {} + virtual void visit(Recovery &) {} + virtual void visit(Cut &) {} +}; + +struct TraversalVisitor : public Ope::Visitor { + using Ope::Visitor::visit; + void visit(Sequence &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(PrioritizedChoice &ope) override { + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { ope.ope_->accept(*this); } + void visit(AndPredicate &ope) override { ope.ope_->accept(*this); } + void visit(NotPredicate &ope) override { ope.ope_->accept(*this); } + void visit(CaptureScope &ope) override { ope.ope_->accept(*this); } + void visit(Capture &ope) override { ope.ope_->accept(*this); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Whitespace &ope) override { ope.ope_->accept(*this); } + void visit(Recovery &ope) override { ope.ope_->accept(*this); } + void visit(PrecedenceClimbing &ope) override { ope.atom_->accept(*this); } +}; + +struct TraceOpeName : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(Sequence &) override { name_ = "Sequence"; } + void visit(PrioritizedChoice &) override { name_ = "PrioritizedChoice"; } + void visit(Repetition &) override { name_ = "Repetition"; } + void visit(AndPredicate &) override { name_ = "AndPredicate"; } + void visit(NotPredicate &) override { name_ = "NotPredicate"; } + void visit(Dictionary &) override { name_ = "Dictionary"; } + void visit(LiteralString &) override { name_ = "LiteralString"; } + void visit(CharacterClass &) override { name_ = "CharacterClass"; } + void visit(Character &) override { name_ = "Character"; } + void visit(AnyCharacter &) override { name_ = "AnyCharacter"; } + void visit(CaptureScope &) override { name_ = "CaptureScope"; } + void visit(Capture &) override { name_ = "Capture"; } + void visit(TokenBoundary &) override { name_ = "TokenBoundary"; } + void visit(Ignore &) override { name_ = "Ignore"; } + void visit(User &) override { name_ = "User"; } + void visit(WeakHolder &) override { name_ = "WeakHolder"; } + void visit(Holder &ope) override { name_ = ope.trace_name().data(); } + void visit(Reference &) override { name_ = "Reference"; } + void visit(Whitespace &) override { name_ = "Whitespace"; } + void visit(BackReference &) override { name_ = "BackReference"; } + void visit(PrecedenceClimbing &) override { name_ = "PrecedenceClimbing"; } + void visit(Recovery &) override { name_ = "Recovery"; } + void visit(Cut &) override { name_ = "Cut"; } + + static std::string get(Ope &ope) { + TraceOpeName vis; + ope.accept(vis); + return vis.name_; + } + +private: + const char *name_ = nullptr; +}; + +struct AssignIDToDefinition : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Holder &ope) override; + void visit(Reference &ope) override; + void visit(PrecedenceClimbing &ope) override; + + std::unordered_map ids; +}; + +struct IsLiteralToken : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + if (!IsLiteralToken::check(*op)) { return; } + } + result_ = true; + } + + void visit(Dictionary &) override { result_ = true; } + void visit(LiteralString &) override { result_ = true; } + + static bool check(Ope &ope) { + IsLiteralToken vis; + ope.accept(vis); + return vis.result_; + } + +private: + bool result_ = false; +}; + +struct TokenChecker : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(TokenBoundary &) override { has_token_boundary_ = true; } + void visit(AndPredicate &) override {} + void visit(NotPredicate &) override {} + void visit(WeakHolder &) override { has_rule_ = true; } + void visit(Reference &ope) override; + + static bool is_token(Ope &ope) { + if (IsLiteralToken::check(ope)) { return true; } + + TokenChecker vis; + ope.accept(vis); + return vis.has_token_boundary_ || !vis.has_rule_; + } + +private: + bool has_token_boundary_ = false; + bool has_rule_ = false; +}; + +struct FindLiteralToken : public Ope::Visitor { + using Ope::Visitor::visit; + + void visit(LiteralString &ope) override { token_ = ope.lit_.data(); } + void visit(TokenBoundary &ope) override { ope.ope_->accept(*this); } + void visit(Ignore &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Recovery &ope) override { ope.ope_->accept(*this); } + + static const char *token(Ope &ope) { + FindLiteralToken vis; + ope.accept(vis); + return vis.token_; + } + +private: + const char *token_ = nullptr; +}; + +struct DetectLeftRecursion : public TraversalVisitor { + using TraversalVisitor::visit; + + DetectLeftRecursion(const std::string &name) : name_(name) {} + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (done_) { + break; + } else if (error_s) { + done_ = true; + break; + } + } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (error_s) { + done_ = true; + break; + } + } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + done_ = ope.min_ > 0; + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + done_ = false; + } + void visit(Dictionary &) override { done_ = true; } + void visit(LiteralString &ope) override { done_ = !ope.lit_.empty(); } + void visit(CharacterClass &) override { done_ = true; } + void visit(Character &) override { done_ = true; } + void visit(AnyCharacter &) override { done_ = true; } + void visit(User &) override { done_ = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { done_ = true; } + void visit(Cut &) override { done_ = true; } + + const char *error_s = nullptr; + + std::shared_ptr resolve_macro_arg(size_t iarg) const; + +private: + std::string name_; + std::unordered_set refs_; + bool done_ = false; + std::vector> *> macro_args_stack_; +}; + +struct ComputeCanBeEmpty : public TraversalVisitor { + using TraversalVisitor::visit; + + bool result = false; + + void visit(Sequence &ope) override { + result = std::all_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(PrioritizedChoice &ope) override { + result = std::any_of(ope.opes_.begin(), ope.opes_.end(), [](auto &op) { + ComputeCanBeEmpty vis; + op->accept(vis); + return vis.result; + }); + } + void visit(Repetition &ope) override { result = ope.min_ == 0; } + void visit(AndPredicate &) override { result = true; } + void visit(NotPredicate &) override { result = true; } + void visit(Dictionary &) override { result = false; } + void visit(LiteralString &ope) override { result = ope.lit_.empty(); } + void visit(CharacterClass &) override { result = false; } + void visit(Character &) override { result = false; } + void visit(AnyCharacter &) override { result = false; } + void visit(User &) override { result = false; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result = false; } + void visit(Cut &) override { result = false; } +}; + +struct HasEmptyElement : public TraversalVisitor { + using TraversalVisitor::visit; + + HasEmptyElement(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} + + void visit(Sequence &ope) override; + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (is_empty) { return; } + } + } + void visit(Repetition &ope) override { + if (ope.min_ == 0) { + set_error(); + } else { + ope.ope_->accept(*this); + } + } + void visit(AndPredicate &) override { set_error(); } + void visit(NotPredicate &) override { set_error(); } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { set_error(); } + } + void visit(Reference &ope) override; + + bool is_empty = false; + const char *error_s = nullptr; + std::string error_name; + +private: + void set_error() { + is_empty = true; + tie(error_s, error_name) = refs_.back(); + } + std::vector> &refs_; + std::unordered_map &has_error_cache_; +}; + +struct DetectInfiniteLoop : public TraversalVisitor { + using TraversalVisitor::visit; + + DetectInfiniteLoop(const char *s, const std::string &name, + std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) { + refs_.emplace_back(s, name); + } + + DetectInfiniteLoop(std::vector> &refs, + std::unordered_map &has_error_cache) + : refs_(refs), has_error_cache_(has_error_cache) {} + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } + } + } + void visit(PrioritizedChoice &ope) override { + for (const auto &op : ope.opes_) { + op->accept(*this); + if (has_error) { return; } + } + } + void visit(Repetition &ope) override { + if (ope.max_ == std::numeric_limits::max()) { + HasEmptyElement vis(refs_, has_error_cache_); + ope.ope_->accept(vis); + if (vis.is_empty) { + has_error = true; + error_s = vis.error_s; + error_name = vis.error_name; + } + } else { + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override; + + bool has_error = false; + const char *error_s = nullptr; + std::string error_name; + +private: + std::vector> &refs_; + std::unordered_map &has_error_cache_; +}; + +struct ReferenceChecker : public TraversalVisitor { + using TraversalVisitor::visit; + + ReferenceChecker(const Grammar &grammar, + const std::vector ¶ms) + : grammar_(grammar), params_(params) {} + + void visit(Reference &ope) override; + + std::unordered_map error_s; + std::unordered_map error_message; + std::unordered_set referenced; + +private: + const Grammar &grammar_; + const std::vector ¶ms_; +}; + +struct LinkReferences : public TraversalVisitor { + using TraversalVisitor::visit; + + LinkReferences(Grammar &grammar, const std::vector ¶ms) + : grammar_(grammar), params_(params) {} + + void visit(Reference &ope) override; + +private: + Grammar &grammar_; + const std::vector ¶ms_; +}; + +struct FindReference : public Ope::Visitor { + using Ope::Visitor::visit; + + FindReference(const std::vector> &args, + const std::vector ¶ms) + : args_(args), params_(params) {} + + void visit(Sequence &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); + } + found_ope = std::make_shared(opes); + } + void visit(PrioritizedChoice &ope) override { + std::vector> opes; + for (const auto &o : ope.opes_) { + o->accept(*this); + opes.emplace_back(std::move(found_ope)); + } + found_ope = std::make_shared(opes); + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + found_ope = rep(found_ope, ope.min_, ope.max_); + } + void visit(AndPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = apd(found_ope); + } + void visit(NotPredicate &ope) override { + ope.ope_->accept(*this); + found_ope = npd(found_ope); + } + void visit(Dictionary &ope) override { found_ope = ope.shared_from_this(); } + void visit(LiteralString &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(CharacterClass &ope) override { + found_ope = ope.shared_from_this(); + } + void visit(Character &ope) override { found_ope = ope.shared_from_this(); } + void visit(AnyCharacter &ope) override { found_ope = ope.shared_from_this(); } + void visit(CaptureScope &ope) override { + ope.ope_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Capture &ope) override { + ope.ope_->accept(*this); + found_ope = cap(found_ope, ope.match_action_); + } + void visit(TokenBoundary &ope) override { + ope.ope_->accept(*this); + found_ope = tok(found_ope); + } + void visit(Ignore &ope) override { + ope.ope_->accept(*this); + found_ope = ign(found_ope); + } + void visit(WeakHolder &ope) override { ope.weak_.lock()->accept(*this); } + void visit(Holder &ope) override { ope.ope_->accept(*this); } + void visit(Reference &ope) override; + void visit(Whitespace &ope) override { + ope.ope_->accept(*this); + found_ope = wsp(found_ope); + } + void visit(PrecedenceClimbing &ope) override { + ope.atom_->accept(*this); + found_ope = csc(found_ope); + } + void visit(Recovery &ope) override { + ope.ope_->accept(*this); + found_ope = rec(found_ope); + } + void visit(Cut &ope) override { found_ope = ope.shared_from_this(); } + + std::shared_ptr found_ope; + +private: + const std::vector> &args_; + const std::vector ¶ms_; +}; + +/* + * First-Set computation + */ +struct ComputeFirstSet : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override { + for (const auto &op : ope.opes_) { + auto save = result_; + result_ = FirstSet{}; + op->accept(*this); + auto element_fs = result_; + result_ = save; + result_.chars |= element_fs.chars; + if (element_fs.any_char) { result_.any_char = true; } + if (!result_.first_literal) { + result_.first_literal = element_fs.first_literal; + } + if (!result_.first_rule) { result_.first_rule = element_fs.first_rule; } + if (!element_fs.can_be_empty) { return; } + // This element can be empty, continue to next + } + result_.can_be_empty = true; + } + void visit(PrioritizedChoice &ope) override { + auto save = result_; + for (const auto &op : ope.opes_) { + result_ = FirstSet{}; + op->accept(*this); + save.merge(result_); + } + result_ = save; + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + if (ope.min_ == 0) { result_.can_be_empty = true; } + } + void visit(AndPredicate &) override { result_.can_be_empty = true; } + void visit(NotPredicate &) override { result_.can_be_empty = true; } + void visit(Dictionary &ope) override { + for (const auto &[key, info] : ope.trie_.dic_) { + if (!key.empty()) { + auto ch = static_cast(key[0]); + result_.chars.set(ch); + if (ope.trie_.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + } + void visit(LiteralString &ope) override { + if (ope.lit_.empty()) { + result_.can_be_empty = true; + } else { + auto ch = static_cast(ope.lit_[0]); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + if (!result_.first_literal) { result_.first_literal = ope.lit_.c_str(); } + } + } + void visit(CharacterClass &ope) override { + for (const auto &range : ope.ranges_) { + auto cp1 = range.first; + auto cp2 = range.second; + if (cp1 > 0x7F || cp2 > 0x7F) { + // Non-ASCII range: conservative fallback + result_.any_char = true; + return; + } + for (auto cp = cp1; cp <= cp2; cp++) { + auto ch = static_cast(cp); + result_.chars.set(ch); + if (ope.ignore_case_) { + result_.chars.set(static_cast(std::toupper(ch))); + result_.chars.set(static_cast(std::tolower(ch))); + } + } + } + if (ope.negated_) { + result_.chars.flip(); + result_.any_char = true; // negated class can match non-ASCII + } + } + void visit(Character &ope) override { + if (ope.ch_ > 0x7F) { + result_.any_char = true; + } else { + result_.chars.set(static_cast(ope.ch_)); + } + } + void visit(AnyCharacter &) override { result_.any_char = true; } + void visit(User &) override { result_.any_char = true; } + void visit(Reference &ope) override; + void visit(BackReference &) override { result_.any_char = true; } + void visit(Cut &) override { result_.can_be_empty = true; } + + FirstSet result_; + +private: + std::unordered_set refs_; +}; + +struct SetupFirstSets : public TraversalVisitor { + using TraversalVisitor::visit; + + void visit(Sequence &ope) override; + void setup_keyword_guarded_identifier(Sequence &ope); + + void visit(PrioritizedChoice &ope) override { + ope.first_sets_.clear(); + ope.first_sets_.reserve(ope.opes_.size()); + for (const auto &op : ope.opes_) { + ComputeFirstSet cfs; + op->accept(cfs); + ope.first_sets_.push_back(cfs.result_); + } + for (const auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Repetition &ope) override { + ope.ope_->accept(*this); + // ISpan optimization: detect Repetition + ASCII CharacterClass + auto cc = dynamic_cast(ope.ope_.get()); + if (cc && cc->is_ascii_only()) { ope.span_bitset_ = &cc->ascii_bitset(); } + } + void visit(Reference &ope) override; + +private: + std::unordered_set refs_; +}; + +/* + * Keywords + */ +static const char *WHITESPACE_DEFINITION_NAME = "%whitespace"; +static const char *WORD_DEFINITION_NAME = "%word"; +static const char *RECOVER_DEFINITION_NAME = "%recover"; + +/* + * Definition + */ +class Definition { +public: + struct Result { + bool ret; + bool recovered; + size_t len; + ErrorInfo error_info; + }; + + Definition() : holder_(std::make_shared(this)) {} + + Definition(const Definition &rhs) : name(rhs.name), holder_(rhs.holder_) { + holder_->outer_ = this; + } + + Definition(const std::shared_ptr &ope) + : holder_(std::make_shared(this)) { + *this <= ope; + } + + operator std::shared_ptr() { + return std::make_shared(holder_); + } + + Definition &operator<=(const std::shared_ptr &ope) { + holder_->ope_ = ope; + return *this; + } + + Result parse(const char *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, path, log); + } + + Result parse(const char *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + SemanticValues vs; + return parse_core(s, n, vs, dt, path, log); + } + + Result parse(const char *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse(s, n, dt, path, log); + } + + template + Result parse_and_get_value(const char *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + std::any dt; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); + } + return r; + } + + template + Result parse_and_get_value(const char *s, T &val, const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, val, path, log); + } + + template + Result parse_and_get_value(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + SemanticValues vs; + auto r = parse_core(s, n, vs, dt, path, log); + if (r.ret && !vs.empty() && vs.front().has_value()) { + val = std::any_cast(vs[0]); + } + return r; + } + + template + Result parse_and_get_value(const char *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + auto n = strlen(s); + return parse_and_get_value(s, n, dt, val, path, log); + } + +#if defined(__cpp_lib_char8_t) + Result parse(const char8_t *s, size_t n, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), n, path, log); + } + + Result parse(const char8_t *s, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), path, log); + } + + Result parse(const char8_t *s, size_t n, std::any &dt, + const char *path = nullptr, Log log = nullptr) const { + return parse(reinterpret_cast(s), n, dt, path, log); + } + + Result parse(const char8_t *s, std::any &dt, const char *path = nullptr, + Log log = nullptr) const { + return parse(reinterpret_cast(s), dt, path, log); + } + + template + Result parse_and_get_value(const char8_t *s, size_t n, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, val, path, + log); + } + + template + Result parse_and_get_value(const char8_t *s, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), val, path, + log); + } + + template + Result parse_and_get_value(const char8_t *s, size_t n, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), n, dt, val, + path, log); + } + + template + Result parse_and_get_value(const char8_t *s, std::any &dt, T &val, + const char *path = nullptr, + Log log = nullptr) const { + return parse_and_get_value(reinterpret_cast(s), dt, val, path, + log); + } +#endif + + void operator=(Action a) { action = a; } + + template Definition &operator,(T fn) { + operator=(fn); + return *this; + } + + Definition &operator~() { + ignoreSemanticValue = true; + return *this; + } + + void accept(Ope::Visitor &v) { holder_->accept(v); } + + std::shared_ptr get_core_operator() const { return holder_->ope_; } + + bool is_token() const { + std::call_once(is_token_init_, [this]() { + is_token_ = TokenChecker::is_token(*get_core_operator()); + }); + return is_token_; + } + + std::string name; + const char *s_ = nullptr; + std::pair line_ = {1, 1}; + + Predicate predicate; + + size_t id = 0; + Action action; + std::function + enter; + std::function + leave; + bool ignoreSemanticValue = false; + std::shared_ptr whitespaceOpe; + std::shared_ptr wordOpe; + bool enablePackratParsing = false; + bool is_macro = false; + std::vector params; + bool disable_action = false; + bool is_left_recursive = false; + bool can_be_empty = false; + + TracerEnter tracer_enter; + TracerLeave tracer_leave; + bool verbose_trace = false; + TracerStartOrEnd tracer_start; + TracerStartOrEnd tracer_end; + + std::string error_message; + bool no_ast_opt = false; + + bool eoi_check = true; + + // Per-rule packrat stats (optional, for profiling) + mutable bool collect_packrat_stats = false; + mutable std::vector packrat_stats_; + +private: + friend class Reference; + friend class ParserGenerator; + + Definition &operator=(const Definition &rhs); + Definition &operator=(Definition &&rhs); + + void initialize_definition_ids() const { + std::call_once(definition_ids_init_, [&]() { + AssignIDToDefinition vis; + holder_->accept(vis); + if (whitespaceOpe) { whitespaceOpe->accept(vis); } + if (wordOpe) { wordOpe->accept(vis); } + definition_ids_.swap(vis.ids); + }); + } + + void initialize_packrat_filter() const; + + Result parse_core(const char *s, size_t n, SemanticValues &vs, std::any &dt, + const char *path, Log log) const { + initialize_definition_ids(); + + std::shared_ptr ope = holder_; + + std::any trace_data; + if (tracer_start) { tracer_start(trace_data); } + auto se = scope_exit([&]() { + if (tracer_end) { tracer_end(trace_data); } + }); + + Context c(path, s, n, definition_ids_.size(), whitespaceOpe, wordOpe, + enablePackratParsing, tracer_enter, tracer_leave, trace_data, + verbose_trace, log); + + if (collect_packrat_stats) { + packrat_stats_.resize(definition_ids_.size()); + c.packrat_stats = &packrat_stats_; + } + + if (enablePackratParsing) { + initialize_packrat_filter(); + if (!packrat_filter_.empty()) { + c.packrat_rule_filter = &packrat_filter_; + } + } + + size_t i = 0; + + if (whitespaceOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + auto len = whitespaceOpe->parse(s, n, vs, c, dt); + if (fail(len)) { return Result{false, c.recovered, i, c.error_info}; } + + i = len; + } + + auto len = ope->parse(s + i, n - i, vs, c, dt); + auto ret = success(len); + if (ret) { + i += len; + if (eoi_check) { + if (i < n) { + if (c.error_info.error_pos - c.s < s + i - c.s) { + c.error_info.message_pos = s + i; + c.error_info.message = "expected end of input"; + } + ret = false; + } + } + } + return Result{ret, c.recovered, i, c.error_info}; + } + + std::shared_ptr holder_; + mutable std::once_flag is_token_init_; + mutable bool is_token_ = false; + mutable std::once_flag assign_id_to_definition_init_; + mutable std::once_flag definition_ids_init_; + mutable std::unordered_map definition_ids_; + mutable std::once_flag packrat_filter_init_; + mutable std::vector packrat_filter_; +}; + +/* + * Implementations + */ + +inline size_t parse_literal(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt, const std::string &lit, + std::once_flag &init_is_word, bool &is_word, + bool ignore_case, const std::string &lower_lit) { + size_t i = 0; + for (; i < lit.size(); i++) { + if (i >= n || + (ignore_case ? (static_cast(std::tolower( + static_cast(s[i]))) != lower_lit[i]) + : (s[i] != lit[i]))) { + c.set_error_pos(s, lit.data()); + return static_cast(-1); + } + } + + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + std::call_once(init_is_word, [&]() { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + auto len = + c.wordOpe->parse(lit.data(), lit.size(), dummy_vs, dummy_c, dummy_dt); + is_word = success(len); + }); + + if (is_word) { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { + c.set_error_pos(s, lit.data()); + return len; + } + i += len; + } + } + + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; + + return i; +} + +inline std::pair SemanticValues::line_info() const { + assert(c_); + return c_->line_info(sv_.data()); +} + +inline void ErrorInfo::output_log(const Log &log, const char *s, size_t n) { + if (message_pos) { + if (message_pos > last_output_pos) { + last_output_pos = message_pos; + auto line = line_info(s, message_pos); + std::string msg; + if (auto unexpected_token = heuristic_error_token(s, n, message_pos); + !unexpected_token.empty()) { + msg = replace_all(message, "%t", unexpected_token); + + auto unexpected_char = unexpected_token.substr( + 0, + codepoint_length(unexpected_token.data(), unexpected_token.size())); + + msg = replace_all(msg, "%c", unexpected_char); + } else { + msg = message; + } + log(line.first, line.second, msg, label); + } + } else if (error_pos) { + if (error_pos > last_output_pos) { + last_output_pos = error_pos; + auto line = line_info(s, error_pos); + + std::string msg; + if (expected_tokens.empty()) { + msg = "syntax error."; + } else { + msg = "syntax error"; + + // unexpected token + if (auto unexpected_token = heuristic_error_token(s, n, error_pos); + !unexpected_token.empty()) { + msg += ", unexpected '"; + msg += unexpected_token; + msg += "'"; + } + + auto first_item = true; + size_t i = 0; + while (i < expected_tokens.size()) { + auto [error_literal, error_rule] = expected_tokens[i]; + + // Skip rules start with '_' + if (!(error_rule && error_rule->name[0] == '_')) { + msg += (first_item ? ", expecting " : ", "); + if (error_literal) { + msg += "'"; + msg += error_literal; + msg += "'"; + } else { + msg += "<" + error_rule->name + ">"; + if (label.empty()) { label = error_rule->name; } + } + first_item = false; + } + + i++; + } + msg += "."; + } + log(line.first, line.second, msg, label); + } + } +} + +inline size_t Context::skip_whitespace(const char *a_s, size_t n, + SemanticValues &vs, std::any &dt) { + if (in_token_boundary_count || !whitespaceOpe) { return 0; } + auto save = ignore_trace_state; + ignore_trace_state = !verbose_trace; + auto se = scope_exit([&]() { ignore_trace_state = save; }); + return whitespaceOpe->parse(a_s, n, vs, *this, dt); +} + +inline void Context::set_error_pos(const char *a_s, const char *literal) { + if (log) { + if (error_info.error_pos <= a_s) { + if (error_info.error_pos < a_s || !error_info.keep_previous_token) { + error_info.error_pos = a_s; + error_info.expected_tokens.clear(); + } + + const char *error_literal = nullptr; + const Definition *error_rule = nullptr; + + if (literal) { + error_literal = literal; + } else if (!rule_stack.empty()) { + auto rule = rule_stack.back(); + auto ope = rule->get_core_operator(); + if (auto token = FindLiteralToken::token(*ope); + token && token[0] != '\0') { + error_literal = token; + } + } + + for (auto r : rule_stack) { + error_rule = r; + if (r->is_token()) { break; } + } + + if (error_literal || error_rule) { + error_info.add(error_literal, error_rule); + } + } + } +} + +inline void Context::trace_enter(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt) { + trace_ids.push_back(next_trace_id++); + tracer_enter(ope, a_s, n, vs, *this, dt, trace_data); +} + +inline void Context::trace_leave(const Ope &ope, const char *a_s, size_t n, + const SemanticValues &vs, std::any &dt, + size_t len) { + tracer_leave(ope, a_s, n, vs, *this, dt, len, trace_data); + trace_ids.pop_back(); +} + +inline bool Context::is_traceable(const Ope &ope) const { + if (tracer_enter && tracer_leave) { + if (ignore_trace_state) { return false; } + return !dynamic_cast(&ope); + } + return false; +} + +inline size_t Ope::parse(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + if (c.is_traceable(*this)) { + c.trace_enter(*this, s, n, vs, dt); + auto len = parse_core(s, n, vs, c, dt); + c.trace_leave(*this, s, n, vs, dt, len); + return len; + } + return parse_core(s, n, vs, c, dt); +} + +inline size_t Dictionary::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + size_t id; + auto i = trie_.match(s, n, id); + + if (i == 0) { + c.set_error_pos(s); + return static_cast(-1); + } + + vs.choice_count_ = trie_.items_count(); + vs.choice_ = id; + + // Word check + if (c.wordOpe) { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + { + SemanticValues dummy_vs; + Context dummy_c(nullptr, c.s, c.l, 0, nullptr, nullptr, false, nullptr, + nullptr, nullptr, false, nullptr); + std::any dummy_dt; + + NotPredicate ope(c.wordOpe); + auto len = ope.parse(s + i, n - i, dummy_vs, dummy_c, dummy_dt); + if (fail(len)) { + c.set_error_pos(s); + return len; + } + i += len; + } + } + + // Skip whitespace + auto wl = c.skip_whitespace(s + i, n - i, vs, dt); + if (fail(wl)) { return wl; } + i += wl; + + return i; +} + +inline size_t LiteralString::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + return parse_literal(s, n, vs, c, dt, lit_, init_is_word_, is_word_, + ignore_case_, lower_lit_); +} + +inline size_t TokenBoundary::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + auto save_ignore_trace_state = c.ignore_trace_state; + c.ignore_trace_state = !c.verbose_trace; + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + size_t len; + { + c.in_token_boundary_count++; + auto se = scope_exit([&]() { c.in_token_boundary_count--; }); + len = ope_->parse(s, n, vs, c, dt); + } + + if (success(len)) { + vs.tokens.emplace_back(std::string_view(s, len)); + + auto wl = c.skip_whitespace(s + len, n - len, vs, dt); + if (fail(wl)) { return wl; } + len += wl; + } + return len; +} + +inline size_t Holder::parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + if (!ope_) { + throw std::logic_error("Uninitialized definition ope was used..."); + } + + // Macro reference + if (outer_->is_macro) { + c.rule_stack.push_back(outer_); + auto len = ope_->parse(s, n, vs, c, dt); + c.rule_stack.pop_back(); + return len; + } + + size_t len; + std::any val; + + // Shared parse body: invokes enter/leave callbacks, parses the rule's + // operator, handles actions/predicates/errors, and calls reduce. + // Returns {parse_len, parse_val}. + auto do_parse = [&]() { + size_t parse_len; + std::any parse_val; + + if (outer_->enter) { outer_->enter(c, s, n, dt); } + auto &chvs = c.push_semantic_values_scope(); + auto se = scope_exit([&]() { + c.pop_semantic_values_scope(); + if (outer_->leave) { outer_->leave(c, s, n, parse_len, parse_val, dt); } + }); + + c.rule_stack.push_back(outer_); + parse_len = ope_->parse(s, n, chvs, c, dt); + c.rule_stack.pop_back(); + + if (success(parse_len)) { + chvs.sv_ = std::string_view(s, parse_len); + chvs.name_ = outer_->name; + + auto ope_ptr = ope_.get(); + if (ope_ptr->is_token_boundary) { + ope_ptr = static_cast(ope_ptr)->ope_.get(); + } + if (!ope_ptr->is_choice_like) { + chvs.choice_count_ = 0; + chvs.choice_ = 0; + } + + std::string msg; + std::any predicate_data; + if (outer_->predicate) { + if (!outer_->predicate(chvs, dt, msg, predicate_data)) { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + parse_len = static_cast(-1); + } + } + + if (success(parse_len)) { + if (!c.recovered) { parse_val = reduce(chvs, dt, predicate_data); } + } else { + if (c.log && !msg.empty() && c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = msg; + c.error_info.label = outer_->name; + } + } + } else { + if (c.log && !outer_->error_message.empty() && + c.error_info.message_pos < s) { + c.error_info.message_pos = s; + c.error_info.message = outer_->error_message; + c.error_info.label = outer_->name; + } + } + + return std::make_pair(parse_len, std::move(parse_val)); + }; + + if (outer_->is_left_recursive) { + auto lr_key = std::make_pair(outer_, s); + + // Check LR memo first + auto it = c.lr_memo.find(lr_key); + if (it != c.lr_memo.end()) { + if (success(it->second.len)) { + len = it->second.len; + val = it->second.val; + } else { + len = static_cast(-1); + } + // Record that this rule's lr_memo was accessed. + // Any LR rule currently seeding will know we're in its cycle. + c.lr_refs_hit.insert(outer_); + } else { + // Seed with FAIL + c.lr_memo[lr_key] = {static_cast(-1), {}}; + + // Mark as active seed (protects our lr_memo from inner growers) + c.lr_active_seeds.insert(lr_key); + auto seed_guard = scope_exit([&]() { c.lr_active_seeds.erase(lr_key); }); + + // Track which LR rules are referenced during our parse + // to identify cycle members + auto saved_refs = std::move(c.lr_refs_hit); + c.lr_refs_hit.clear(); + + // Initial parse (self-references will hit the FAIL seed) + auto [initial_len, initial_val] = do_parse(); + + // Rules whose lr_memo was hit during our parse are in our cycle. + // If we detected cycle members, we ourselves are also part of + // the cycle, so add self — this lets parent seeders see us as + // a transitive cycle member. + auto cycle_rules = c.lr_refs_hit; + if (!cycle_rules.empty()) { cycle_rules.insert(outer_); } + + // Restore parent's refs and propagate cycle info upward + c.lr_refs_hit = std::move(saved_refs); + c.lr_refs_hit.insert(cycle_rules.begin(), cycle_rules.end()); + + if (!success(initial_len)) { + // Keep FAIL in lr_memo so we don't re-seed + len = static_cast(-1); + } else { + // Got initial seed, now grow + len = initial_len; + val = std::move(initial_val); + c.lr_memo[lr_key] = {len, val}; + + while (true) { + // Clear this rule's packrat cache + c.clear_packrat_cache(s, outer_->id); + + // Clear lr_memo for cycle-dependent rules at this position, + // but NOT for rules currently in their own seeding phase + // (lr_active_seeds) — those are outer growers we must not + // interfere with. + for (auto memo_it = c.lr_memo.begin(); memo_it != c.lr_memo.end();) { + if (memo_it->first.second == s && memo_it->first.first != outer_ && + cycle_rules.count(memo_it->first.first) && + !c.lr_active_seeds.count(memo_it->first)) { + memo_it = c.lr_memo.erase(memo_it); + } else { + ++memo_it; + } + } + + auto [new_len, new_val] = do_parse(); + + if (!success(new_len) || new_len <= len) { + break; // No improvement, done growing + } + + len = new_len; + val = std::move(new_val); + c.lr_memo[lr_key] = {len, val}; + } + } + + // Write final result to packrat cache (lr_memo entry is kept as + // the primary lookup for LR rules at this position) + if (success(len)) { c.write_packrat_cache(s, outer_->id, len, val); } + } + } else { + if (c.enablePackratParsing) { + // Packrat cache acts as re-entry guard (pre-registered as + // failure before fn is called). + c.packrat(s, outer_->id, len, val, [&](std::any &a_val) { + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + if (success(len)) { a_val = std::move(parse_val); } + }); + } else { + // Without packrat, use lr_memo as re-entry guard to prevent + // stack overflow from undetected left recursion. + auto guard_key = std::make_pair(outer_, s); + if (c.lr_memo.count(guard_key)) { + len = static_cast(-1); + } else { + c.lr_memo[guard_key] = {static_cast(-1), {}}; + auto [parse_len, parse_val] = do_parse(); + len = parse_len; + val = std::move(parse_val); + c.lr_memo.erase(guard_key); + } + } + } + + if (success(len)) { + if (!outer_->ignoreSemanticValue) { + vs.emplace_back(std::move(val)); + vs.tags.emplace_back(str2tag(outer_->name)); + } + } + + return len; +} + +inline std::any Holder::reduce(SemanticValues &vs, std::any &dt, + const std::any &predicate_data) const { + if (outer_->action && !outer_->disable_action) { + return outer_->action(vs, dt, predicate_data); + } else if (vs.empty()) { + return std::any(); + } else { + return std::move(vs.front()); + } +} + +inline const std::string &Holder::name() const { return outer_->name; } + +inline const std::string &Holder::trace_name() const { + std::call_once(trace_name_init_, + [this]() { trace_name_ = "[" + outer_->name + "]"; }); + return trace_name_; +} + +inline size_t Reference::parse_core(const char *s, size_t n, SemanticValues &vs, + Context &c, std::any &dt) const { + auto save_ignore_trace_state = c.ignore_trace_state; + if (rule_ && rule_->ignoreSemanticValue) { + c.ignore_trace_state = !c.verbose_trace; + } + auto se = + scope_exit([&]() { c.ignore_trace_state = save_ignore_trace_state; }); + + if (rule_) { + // Reference rule + if (rule_->is_macro) { + // Macro + FindReference vis(c.top_args(), c.rule_stack.back()->params); + + // Collect arguments + std::vector> args; + for (const auto &arg : args_) { + arg->accept(vis); + args.emplace_back(std::move(vis.found_ope)); + } + + c.push_args(std::move(args)); + auto se = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); + } else { + // Definition + c.push_args(std::vector>()); + auto se2 = scope_exit([&]() { c.pop_args(); }); + return rule_->holder_->parse(s, n, vs, c, dt); + } + } else { + // Reference parameter in macro + const auto &args = c.top_args(); + return args[iarg_]->parse(s, n, vs, c, dt); + } +} + +inline std::shared_ptr Reference::get_core_operator() const { + return rule_->holder_; +} + +inline size_t BackReference::parse_core(const char *s, size_t n, + SemanticValues &vs, Context &c, + std::any &dt) const { + for (auto it = c.capture_entries.rbegin(); it != c.capture_entries.rend(); + ++it) { + if (it->first == name_) { + const auto &lit = it->second; + std::once_flag init_is_word; + auto is_word = false; + static const std::string empty; + return parse_literal(s, n, vs, c, dt, lit, init_is_word, is_word, false, + empty); + } + } + + c.error_info.message_pos = s; + c.error_info.message = "undefined back reference '$" + name_ + "'..."; + return static_cast(-1); +} + +inline Definition & +PrecedenceClimbing::get_reference_for_binop(Context &c) const { + if (rule_.is_macro) { + // Reference parameter in macro + const auto &args = c.top_args(); + auto iarg = dynamic_cast(*binop_).iarg_; + auto arg = args[iarg]; + return *dynamic_cast(*arg).rule_; + } + + return *dynamic_cast(*binop_).rule_; +} + +inline size_t PrecedenceClimbing::parse_expression(const char *s, size_t n, + SemanticValues &vs, + Context &c, std::any &dt, + size_t min_prec) const { + auto len = atom_->parse(s, n, vs, c, dt); + if (fail(len)) { return len; } + + std::string tok; + auto &rule = get_reference_for_binop(c); + auto action = std::move(rule.action); + + rule.action = [&](SemanticValues &vs2, std::any &dt2, + const std::any &predicate_data2) { + tok = vs2.token(); + if (action) { + return action(vs2, dt2, predicate_data2); + } else if (!vs2.empty()) { + return vs2[0]; + } + return std::any(); + }; + auto action_se = scope_exit([&]() { rule.action = std::move(action); }); + + auto i = len; + while (i < n) { + std::vector save_values(vs.begin(), vs.end()); + auto save_tokens = vs.tokens; + + auto chvs = c.push_semantic_values_scope(); + auto chlen = binop_->parse(s + i, n - i, chvs, c, dt); + c.pop_semantic_values_scope(); + + if (fail(chlen)) { break; } + + auto it = info_.find(tok); + if (it == info_.end()) { break; } + + auto level = std::get<0>(it->second); + auto assoc = std::get<1>(it->second); + + if (level < min_prec) { break; } + + vs.emplace_back(std::move(chvs[0])); + i += chlen; + + auto next_min_prec = level; + if (assoc == 'L') { next_min_prec = level + 1; } + + chvs = c.push_semantic_values_scope(); + chlen = parse_expression(s + i, n - i, chvs, c, dt, next_min_prec); + c.pop_semantic_values_scope(); + + if (fail(chlen)) { + vs.assign(save_values.begin(), save_values.end()); + vs.tokens = save_tokens; + i = chlen; + break; + } + + vs.emplace_back(std::move(chvs[0])); + i += chlen; + + std::any val; + if (rule_.action) { + vs.sv_ = std::string_view(s, i); + static const std::any empty_predicate_data; + val = rule_.action(vs, dt, empty_predicate_data); + } else if (!vs.empty()) { + val = vs[0]; + } + vs.clear(); + vs.emplace_back(std::move(val)); + } + + return i; +} + +inline size_t Recovery::parse_core(const char *s, size_t n, + SemanticValues & /*vs*/, Context &c, + std::any & /*dt*/) const { + const auto &rule = dynamic_cast(*ope_); + + // Custom error message + if (c.log) { + auto label = dynamic_cast(rule.args_[0].get()); + if (label && !label->rule_->error_message.empty()) { + c.error_info.message_pos = s; + c.error_info.message = label->rule_->error_message; + c.error_info.label = label->rule_->name; + } + } + + // Recovery + auto len = static_cast(-1); + { + auto save_log = c.log; + c.log = nullptr; + auto se = scope_exit([&]() { c.log = save_log; }); + + SemanticValues dummy_vs; + std::any dummy_dt; + + len = rule.parse(s, n, dummy_vs, c, dummy_dt); + } + + if (success(len)) { + c.recovered = true; + + if (c.log) { + c.error_info.output_log(c.log, c.s, c.l); + c.error_info.clear(); + } + } + + // Cut + if (!c.cut_stack.empty()) { + c.cut_stack.back() = true; + + if (c.cut_stack.size() == 1) { + // TODO: Remove unneeded entries in packrat memoise table + } + } + + return len; +} + +inline void Sequence::accept(Visitor &v) { v.visit(*this); } +inline void PrioritizedChoice::accept(Visitor &v) { v.visit(*this); } +inline void Repetition::accept(Visitor &v) { v.visit(*this); } +inline void AndPredicate::accept(Visitor &v) { v.visit(*this); } +inline void NotPredicate::accept(Visitor &v) { v.visit(*this); } +inline void Dictionary::accept(Visitor &v) { v.visit(*this); } +inline void LiteralString::accept(Visitor &v) { v.visit(*this); } +inline void CharacterClass::accept(Visitor &v) { v.visit(*this); } +inline void Character::accept(Visitor &v) { v.visit(*this); } +inline void AnyCharacter::accept(Visitor &v) { v.visit(*this); } +inline void CaptureScope::accept(Visitor &v) { v.visit(*this); } +inline void Capture::accept(Visitor &v) { v.visit(*this); } +inline void TokenBoundary::accept(Visitor &v) { v.visit(*this); } +inline void Ignore::accept(Visitor &v) { v.visit(*this); } +inline void User::accept(Visitor &v) { v.visit(*this); } +inline void WeakHolder::accept(Visitor &v) { v.visit(*this); } +inline void Holder::accept(Visitor &v) { v.visit(*this); } +inline void Reference::accept(Visitor &v) { v.visit(*this); } +inline void Whitespace::accept(Visitor &v) { v.visit(*this); } +inline void BackReference::accept(Visitor &v) { v.visit(*this); } +inline void PrecedenceClimbing::accept(Visitor &v) { v.visit(*this); } +inline void Recovery::accept(Visitor &v) { v.visit(*this); } +inline void Cut::accept(Visitor &v) { v.visit(*this); } + +inline void AssignIDToDefinition::visit(Holder &ope) { + auto p = static_cast(ope.outer_); + if (ids.count(p)) { return; } + auto id = ids.size(); + ids[p] = id; + ope.outer_->id = id; + ope.ope_->accept(*this); +} + +inline void AssignIDToDefinition::visit(Reference &ope) { + if (ope.rule_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + ope.rule_->accept(*this); + } +} + +inline void AssignIDToDefinition::visit(PrecedenceClimbing &ope) { + ope.atom_->accept(*this); + ope.binop_->accept(*this); +} + +inline void TokenChecker::visit(Reference &ope) { + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } else { + has_rule_ = true; + } +} + +inline void FindLiteralToken::visit(Reference &ope) { + if (ope.is_macro_) { + ope.rule_->accept(*this); + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ComputeCanBeEmpty::visit(Reference &ope) { + result = ope.rule_ && ope.rule_->can_be_empty; +} + +inline void DetectLeftRecursion::visit(Reference &ope) { + if (ope.name_ == name_) { + error_s = ope.s_; + } else if (!ope.rule_ && !macro_args_stack_.empty()) { + // Macro parameter reference: resolve through nested macro arg + // stacks (e.g. B(X) <- C(X) where X is itself a param ref). + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + resolved->accept(*this); + if (done_ == false) { return; } + } + } else if (!refs_.count(ope.name_)) { + refs_.insert(ope.name_); + if (ope.rule_) { + if (ope.is_macro_) { macro_args_stack_.push_back(&ope.args_); } + ope.rule_->accept(*this); + if (ope.is_macro_) { macro_args_stack_.pop_back(); } + if (done_ == false) { return; } + } + } + // If the referenced rule can match empty, don't mark as done — + // the sequence may continue past this element to find LR. + if (!ope.rule_ && !macro_args_stack_.empty()) { + auto resolved = resolve_macro_arg(ope.iarg_); + if (resolved) { + ComputeCanBeEmpty cbe; + resolved->accept(cbe); + done_ = !cbe.result; + } else { + done_ = true; + } + } else { + done_ = !(ope.rule_ && ope.rule_->can_be_empty); + } +} + +inline std::shared_ptr +DetectLeftRecursion::resolve_macro_arg(size_t iarg) const { + for (int i = static_cast(macro_args_stack_.size()) - 1; i >= 0; i--) { + auto &args = *macro_args_stack_[i]; + if (iarg >= args.size()) { return nullptr; } + auto ref = dynamic_cast(args[iarg].get()); + if (ref && !ref->rule_) { + // Another param ref — resolve using parent level's args + iarg = ref->iarg_; + continue; + } + return args[iarg]; + } + return nullptr; +} + +inline void HasEmptyElement::visit(Sequence &ope) { + auto save_is_empty = false; + const char *save_error_s = nullptr; + std::string save_error_name; + + auto it = ope.opes_.begin(); + while (it != ope.opes_.end()) { + (*it)->accept(*this); + if (!is_empty) { + ++it; + while (it != ope.opes_.end()) { + DetectInfiniteLoop vis(refs_, has_error_cache_); + (*it)->accept(vis); + if (vis.has_error) { + is_empty = true; + error_s = vis.error_s; + error_name = vis.error_name; + } + ++it; + } + return; + } + + save_is_empty = is_empty; + save_error_s = error_s; + save_error_name = error_name; + + is_empty = false; + error_name.clear(); + ++it; + } + + is_empty = save_is_empty; + error_s = save_error_s; + error_name = save_error_name; +} + +inline void HasEmptyElement::visit(Reference &ope) { + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } + + if (ope.rule_) { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + } +} + +inline void DetectInfiniteLoop::visit(Reference &ope) { + auto it = std::find_if(refs_.begin(), refs_.end(), + [&](const std::pair &ref) { + return ope.name_ == ref.second; + }); + if (it != refs_.end()) { return; } + + if (ope.rule_) { + auto it = has_error_cache_.find(ope.name_); + if (it != has_error_cache_.end()) { + has_error = it->second; + } else { + refs_.emplace_back(ope.s_, ope.name_); + ope.rule_->accept(*this); + refs_.pop_back(); + has_error_cache_[ope.name_] = has_error; + } + } + + if (ope.is_macro_) { + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ReferenceChecker::visit(Reference &ope) { + auto it = std::find(params_.begin(), params_.end(), ope.name_); + if (it != params_.end()) { return; } + + if (!grammar_.count(ope.name_)) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not defined."; + } else { + if (!referenced.count(ope.name_)) { referenced.insert(ope.name_); } + const auto &rule = grammar_.at(ope.name_); + if (rule.is_macro) { + if (!ope.is_macro_ || ope.args_.size() != rule.params.size()) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "incorrect number of arguments."; + } + } else if (ope.is_macro_) { + error_s[ope.name_] = ope.s_; + error_message[ope.name_] = "'" + ope.name_ + "' is not macro."; + } + for (const auto &arg : ope.args_) { + arg->accept(*this); + } + } +} + +inline void ComputeFirstSet::visit(Reference &ope) { + if (!ope.rule_) { + // Macro parameter reference — can't predict what it will match + result_.any_char = true; + return; + } + if (refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + if (!result_.first_rule && ope.rule_->is_token()) { + result_.first_rule = ope.rule_; + } + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Reference &ope) { + if (!ope.rule_ || refs_.count(ope.name_)) { return; } + refs_.insert(ope.name_); + ope.rule_->accept(*this); + refs_.erase(ope.name_); +} + +inline void SetupFirstSets::visit(Sequence &ope) { + ope.kw_guard_.reset(); + setup_keyword_guarded_identifier(ope); + for (const auto &op : ope.opes_) { + op->accept(*this); + } +} + +inline void SetupFirstSets::setup_keyword_guarded_identifier(Sequence &seq) { + // Detect pattern: NotPredicate(Reference→PrioritizedChoice) + // TokenBoundary(Sequence[CharacterClass, + // Repetition(CharacterClass)]) + // This is the pattern used by: PlainIdentifier <- !ReservedKeyword + // <[a-z_]i[a-z0-9_]i*> + if (seq.opes_.size() != 2) { return; } + + // Child 0 must be NotPredicate + auto *not_pred = dynamic_cast(seq.opes_[0].get()); + if (!not_pred) { return; } + + // NotPredicate's child must be Reference to a rule + auto *ref = dynamic_cast(not_pred->ope_.get()); + if (!ref || !ref->rule_) { return; } + + // The referenced rule's inner operator (Holder) must contain + // PrioritizedChoice + auto *holder = dynamic_cast(ref->get_core_operator().get()); + if (!holder) { return; } + auto *choice = dynamic_cast(holder->ope_.get()); + if (!choice) { return; } + + // Extract keywords from PrioritizedChoice alternatives + std::vector exact_keywords; + std::vector prefix_keywords; + + for (const auto &alt : choice->opes_) { + auto *lit = dynamic_cast(alt.get()); + if (lit) { + if (!lit->ignore_case_) { return; } + exact_keywords.push_back(to_lower(lit->lit_)); + continue; + } + // Check for compound keyword (Sequence of LiteralStrings) + auto *sub_seq = dynamic_cast(alt.get()); + if (sub_seq && !sub_seq->opes_.empty()) { + auto *first_lit = dynamic_cast(sub_seq->opes_[0].get()); + if (first_lit) { + auto all_ignore_case_lits = + std::all_of(sub_seq->opes_.begin(), sub_seq->opes_.end(), + [](const auto &child) { + auto *l = dynamic_cast(child.get()); + return l && l->ignore_case_; + }); + if (all_ignore_case_lits) { + prefix_keywords.push_back(to_lower(first_lit->lit_)); + continue; + } + } + } + // Unrecognized alternative — bail out + return; + } + + if (exact_keywords.empty()) { return; } + + // Child 1 must be TokenBoundary + auto *tb = dynamic_cast(seq.opes_[1].get()); + if (!tb) { return; } + + // TokenBoundary content: Sequence[CharacterClass, Repetition(CharacterClass)] + // or just CharacterClass (single char identifier) + CharacterClass *first_cc = nullptr; + CharacterClass *rest_cc = nullptr; + + auto *inner_seq = dynamic_cast(tb->ope_.get()); + if (inner_seq && inner_seq->opes_.size() == 2) { + first_cc = dynamic_cast(inner_seq->opes_[0].get()); + auto *rep = dynamic_cast(inner_seq->opes_[1].get()); + if (rep) { rest_cc = dynamic_cast(rep->ope_.get()); } + } + + if (!first_cc || !rest_cc) { return; } + if (!first_cc->is_ascii_only() || !rest_cc->is_ascii_only()) { return; } + + // All conditions met — set up the fast path + auto kw = std::make_unique(); + kw->identifier_first = first_cc->ascii_bitset(); + kw->identifier_rest = rest_cc->ascii_bitset(); + + // Compute keyword length range for early-out in hot path + size_t min_len = SIZE_MAX, max_len = 0; + for (const auto &k : exact_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + for (const auto &k : prefix_keywords) { + min_len = std::min(min_len, k.size()); + max_len = std::max(max_len, k.size()); + } + kw->min_keyword_len = min_len; + kw->max_keyword_len = max_len; + + kw->exact_keywords = std::move(exact_keywords); + kw->prefix_keywords = std::move(prefix_keywords); + seq.kw_guard_ = std::move(kw); +} + +// Compute which rules benefit from packrat memoization. +// A rule benefits if it's reachable from 2+ alternatives of the same +// PrioritizedChoice (backtracking will re-visit it at the same position). +inline void Definition::initialize_packrat_filter() const { + std::call_once(packrat_filter_init_, [&]() { + auto def_count = definition_ids_.size(); + if (def_count == 0) { return; } + + // Collect rule IDs reachable from an Ope subtree (bitvector indexed by + // def_id) + struct CollectReachableRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector reachable; // indexed by def_id + + CollectReachableRules(size_t n) : reachable(n, false) {} + + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < reachable.size()) { reachable[id] = true; } + ope.ope_->accept(*this); + } + void visit(Reference &ope) override { + if (ope.rule_ && ope.rule_->id < reachable.size() && + !reachable[ope.rule_->id]) { + reachable[ope.rule_->id] = true; + ope.rule_->accept(*this); + } + } + }; + + // Find rules that benefit: reachable from 2+ alternatives of same choice + std::vector benefits(def_count, false); + + struct FindBacktrackRules : public TraversalVisitor { + using TraversalVisitor::visit; + std::vector &benefits; + size_t def_count; + std::vector visited_rules; // indexed by def_id + + FindBacktrackRules(std::vector &b, size_t n) + : benefits(b), def_count(n), visited_rules(n, false) {} + + void visit(PrioritizedChoice &ope) override { + // For each alternative, collect reachable rules as bitvectors + std::vector> alt_reachable; + for (auto &op : ope.opes_) { + CollectReachableRules crr(def_count); + op->accept(crr); + alt_reachable.push_back(std::move(crr.reachable)); + } + + // Mark rules reachable from 2+ alternatives + for (size_t id = 0; id < def_count; id++) { + size_t count = 0; + for (auto &alt : alt_reachable) { + if (alt[id]) { count++; } + } + if (count >= 2) { benefits[id] = true; } + } + + // Recurse into alternatives + for (auto &op : ope.opes_) { + op->accept(*this); + } + } + void visit(Holder &ope) override { + auto id = ope.outer_->id; + if (id < visited_rules.size() && !visited_rules[id]) { + visited_rules[id] = true; + ope.ope_->accept(*this); + } + } + void visit(Reference &ope) override { + if (ope.rule_) { ope.rule_->accept(*this); } + } + }; + + FindBacktrackRules finder(benefits, def_count); + holder_->accept(finder); + if (whitespaceOpe) { whitespaceOpe->accept(finder); } + if (wordOpe) { wordOpe->accept(finder); } + + packrat_filter_ = std::move(benefits); + }); +} + +inline void LinkReferences::visit(Reference &ope) { + // Check if the reference is a macro parameter + auto found_param = false; + for (size_t i = 0; i < params_.size(); i++) { + const auto ¶m = params_[i]; + if (param == ope.name_) { + ope.iarg_ = i; + found_param = true; + break; + } + } + + // Check if the reference is a definition rule + if (!found_param && grammar_.count(ope.name_)) { + auto &rule = grammar_.at(ope.name_); + ope.rule_ = &rule; + } + + for (const auto &arg : ope.args_) { + arg->accept(*this); + } +} + +inline void FindReference::visit(Reference &ope) { + for (size_t i = 0; i < args_.size(); i++) { + const auto &name = params_[i]; + if (name == ope.name_) { + found_ope = args_[i]; + return; + } + } + found_ope = ope.shared_from_this(); +} + +/*----------------------------------------------------------------------------- + * PEG parser generator + *---------------------------------------------------------------------------*/ + +using Rules = std::unordered_map>; + +class ParserGenerator { +public: + struct ParserContext { + std::shared_ptr grammar; + std::string start; + bool enablePackratParsing = false; + }; + + static ParserContext parse(const char *s, size_t n, const Rules &rules, + Log log, std::string_view start, + bool enable_left_recursion = true) { + return get_instance().perform_core(s, n, rules, log, std::string(start), + enable_left_recursion); + } + + // For debugging purpose + static bool parse_test(const char *d, const char *s) { + Data data; + std::any dt = &data; + + auto n = strlen(s); + auto r = get_instance().g[d].parse(s, n, dt); + return r.ret && r.len == n; + } + +#if defined(__cpp_lib_char8_t) + static bool parse_test(const char *d, const char8_t *s) { + return parse_test(d, reinterpret_cast(s)); + } +#endif + +private: + static ParserGenerator &get_instance() { + static ParserGenerator instance; + return instance; + } + + ParserGenerator() { + make_grammar(); + setup_actions(); + } + + struct Instruction { + std::string type; + std::any data; + std::string_view sv; + }; + + struct Data { + std::shared_ptr grammar; + std::string start; + const char *start_pos = nullptr; + + std::vector> duplicates_of_definition; + + std::vector> duplicates_of_instruction; + std::map> instructions; + + std::vector> undefined_back_references; + std::vector> captures_stack{{}}; + + std::set captures_in_current_definition; + bool enablePackratParsing = true; + + Data() : grammar(std::make_shared()) {} + }; + + class SyntaxErrorException : public std::runtime_error { + public: + SyntaxErrorException(const char *what_arg, std::pair r) + : std::runtime_error(what_arg), r_(r) {} + + std::pair line_info() const { return r_; } + + private: + std::pair r_; + }; + + void make_grammar() { + // Setup PEG syntax parser + g["Grammar"] <= seq(g["Spacing"], oom(g["Definition"]), g["EndOfFile"]); + g["Definition"] <= + cho(seq(g["Ignore"], g["IdentCont"], g["Parameters"], g["LEFTARROW"], + g["Expression"], opt(g["Instruction"])), + seq(g["Ignore"], g["Identifier"], g["LEFTARROW"], g["Expression"], + opt(g["Instruction"]))); + g["Expression"] <= seq(g["Sequence"], zom(seq(g["SLASH"], g["Sequence"]))); + g["Sequence"] <= zom(cho(g["CUT"], g["Prefix"])); + g["Prefix"] <= seq(opt(cho(g["AND"], g["NOT"])), g["SuffixWithLabel"]); + g["SuffixWithLabel"] <= + seq(g["Suffix"], opt(seq(g["LABEL"], g["Identifier"]))); + g["Suffix"] <= seq(g["Primary"], opt(g["Loop"])); + g["Loop"] <= cho(g["QUESTION"], g["STAR"], g["PLUS"], g["Repetition"]); + g["Primary"] <= cho(seq(g["Ignore"], g["IdentCont"], g["Arguments"], + npd(g["LEFTARROW"])), + seq(g["Ignore"], g["Identifier"], + npd(seq(opt(g["Parameters"]), g["LEFTARROW"]))), + seq(g["OPEN"], g["Expression"], g["CLOSE"]), + seq(g["BeginTok"], g["Expression"], g["EndTok"]), + g["CapScope"], + seq(g["BeginCap"], g["Expression"], g["EndCap"]), + g["BackRef"], g["DictionaryI"], g["LiteralI"], + g["Dictionary"], g["Literal"], g["NegatedClassI"], + g["NegatedClass"], g["ClassI"], g["Class"], g["DOT"]); + + g["Identifier"] <= seq(g["IdentCont"], g["Spacing"]); + g["IdentCont"] <= tok(seq(g["IdentStart"], zom(g["IdentRest"]))); + + const static std::vector> range = { + {0x0080, 0xFFFF}}; + g["IdentStart"] <= seq(npd(lit(u8(u8"↑"))), npd(lit(u8(u8"⇑"))), + cho(cls("a-zA-Z_%"), cls(range))); + + g["IdentRest"] <= cho(g["IdentStart"], cls("0-9")); + + g["Dictionary"] <= seq(g["LiteralD"], oom(seq(g["PIPE"], g["LiteralD"]))); + + g["DictionaryI"] <= + seq(g["LiteralID"], oom(seq(g["PIPE"], g["LiteralID"]))); + + auto lit_ope = cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), + cls("'"), g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), + cls("\""), g["Spacing"])); + g["Literal"] <= lit_ope; + g["LiteralD"] <= lit_ope; + + auto lit_case_ignore_ope = + cho(seq(cls("'"), tok(zom(seq(npd(cls("'")), g["Char"]))), lit("'i"), + g["Spacing"]), + seq(cls("\""), tok(zom(seq(npd(cls("\"")), g["Char"]))), lit("\"i"), + g["Spacing"])); + g["LiteralI"] <= lit_case_ignore_ope; + g["LiteralID"] <= lit_case_ignore_ope; + + // NOTE: The original Brian Ford's paper uses 'zom' instead of 'oom'. + g["Class"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["ClassI"] <= seq(chr('['), npd(chr('^')), + tok(oom(seq(npd(chr(']')), g["Range"]))), lit("]i"), + g["Spacing"]); + + g["NegatedClass"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), chr(']'), + g["Spacing"]); + g["NegatedClassI"] <= seq(lit("[^"), + tok(oom(seq(npd(chr(']')), g["Range"]))), + lit("]i"), g["Spacing"]); + + // NOTE: This is different from The original Brian Ford's paper, and this + // modification allows us to specify `[+-]` as a valid char class. + g["Range"] <= + cho(seq(g["Char"], chr('-'), npd(chr(']')), g["Char"]), g["Char"]); + + g["Char"] <= + cho(seq(chr('\\'), cls("fnrtv'\"[]\\^-")), + seq(chr('\\'), cls("0-3"), cls("0-7"), cls("0-7")), + seq(chr('\\'), cls("0-7"), opt(cls("0-7"))), + seq(lit("\\x"), cls("0-9a-fA-F"), opt(cls("0-9a-fA-F"))), + seq(lit("\\u"), + cho(seq(cho(seq(chr('0'), cls("0-9a-fA-F")), lit("10")), + rep(cls("0-9a-fA-F"), 4, 4)), + rep(cls("0-9a-fA-F"), 4, 5))), + seq(npd(chr('\\')), dot())); + + g["Repetition"] <= + seq(g["BeginBracket"], g["RepetitionRange"], g["EndBracket"]); + g["RepetitionRange"] <= cho(seq(g["Number"], g["COMMA"], g["Number"]), + seq(g["Number"], g["COMMA"]), g["Number"], + seq(g["COMMA"], g["Number"])); + g["Number"] <= seq(oom(cls("0-9")), g["Spacing"]); + + g["CapScope"] <= seq(g["BeginCapScope"], g["Expression"], g["EndCapScope"]); + + g["LEFTARROW"] <= seq(cho(lit("<-"), lit(u8(u8"←"))), g["Spacing"]); + ~g["SLASH"] <= seq(chr('/'), g["Spacing"]); + ~g["PIPE"] <= seq(chr('|'), g["Spacing"]); + g["AND"] <= seq(chr('&'), g["Spacing"]); + g["NOT"] <= seq(chr('!'), g["Spacing"]); + g["QUESTION"] <= seq(chr('?'), g["Spacing"]); + g["STAR"] <= seq(chr('*'), g["Spacing"]); + g["PLUS"] <= seq(chr('+'), g["Spacing"]); + ~g["OPEN"] <= seq(chr('('), g["Spacing"]); + ~g["CLOSE"] <= seq(chr(')'), g["Spacing"]); + g["DOT"] <= seq(chr('.'), g["Spacing"]); + + g["CUT"] <= seq(lit(u8(u8"↑")), g["Spacing"]); + ~g["LABEL"] <= seq(cho(chr('^'), lit(u8(u8"⇑"))), g["Spacing"]); + + ~g["Spacing"] <= zom(cho(g["Space"], g["Comment"])); + g["Comment"] <= seq(chr('#'), zom(seq(npd(g["EndOfLine"]), dot())), + opt(g["EndOfLine"])); + g["Space"] <= cho(chr(' '), chr('\t'), g["EndOfLine"]); + g["EndOfLine"] <= cho(lit("\r\n"), chr('\n'), chr('\r')); + g["EndOfFile"] <= npd(dot()); + + ~g["BeginTok"] <= seq(chr('<'), g["Spacing"]); + ~g["EndTok"] <= seq(chr('>'), g["Spacing"]); + + ~g["BeginCapScope"] <= seq(chr('$'), chr('('), g["Spacing"]); + ~g["EndCapScope"] <= seq(chr(')'), g["Spacing"]); + + g["BeginCap"] <= seq(chr('$'), tok(g["IdentCont"]), chr('<'), g["Spacing"]); + ~g["EndCap"] <= seq(chr('>'), g["Spacing"]); + + g["BackRef"] <= seq(chr('$'), tok(g["IdentCont"]), g["Spacing"]); + + g["IGNORE"] <= chr('~'); + + g["Ignore"] <= opt(g["IGNORE"]); + g["Parameters"] <= seq(g["OPEN"], g["Identifier"], + zom(seq(g["COMMA"], g["Identifier"])), g["CLOSE"]); + g["Arguments"] <= seq(g["OPEN"], g["Expression"], + zom(seq(g["COMMA"], g["Expression"])), g["CLOSE"]); + ~g["COMMA"] <= seq(chr(','), g["Spacing"]); + + // Instruction grammars + g["Instruction"] <= + seq(g["BeginBracket"], + opt(seq(g["InstructionItem"], zom(seq(g["InstructionItemSeparator"], + g["InstructionItem"])))), + g["EndBracket"]); + g["InstructionItem"] <= + cho(g["PrecedenceClimbing"], g["ErrorMessage"], g["NoAstOpt"]); + ~g["InstructionItemSeparator"] <= seq(chr(';'), g["Spacing"]); + + ~g["SpacesZom"] <= zom(g["Space"]); + ~g["SpacesOom"] <= oom(g["Space"]); + ~g["BeginBracket"] <= seq(chr('{'), g["Spacing"]); + ~g["EndBracket"] <= seq(chr('}'), g["Spacing"]); + + // PrecedenceClimbing instruction + g["PrecedenceClimbing"] <= + seq(lit("precedence"), g["SpacesOom"], g["PrecedenceInfo"], + zom(seq(g["SpacesOom"], g["PrecedenceInfo"])), g["SpacesZom"]); + g["PrecedenceInfo"] <= + seq(g["PrecedenceAssoc"], + oom(seq(ign(g["SpacesOom"]), g["PrecedenceOpe"]))); + g["PrecedenceOpe"] <= + cho(seq(cls("'"), + tok(zom(seq(npd(cho(g["Space"], cls("'"))), g["Char"]))), + cls("'")), + seq(cls("\""), + tok(zom(seq(npd(cho(g["Space"], cls("\""))), g["Char"]))), + cls("\"")), + tok(oom(seq(npd(cho(g["PrecedenceAssoc"], g["Space"], chr('}'))), + dot())))); + g["PrecedenceAssoc"] <= cls("LR"); + + // Error message instruction + g["ErrorMessage"] <= seq(lit("error_message"), g["SpacesOom"], + g["LiteralD"], g["SpacesZom"]); + + // No Ast node optimization instruction + g["NoAstOpt"] <= seq(lit("no_ast_opt"), g["SpacesZom"]); + + // Set definition names + for (auto &x : g) { + x.second.name = x.first; + } + } + + void setup_actions() { + g["Definition"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + auto name = std::any_cast(vs[1]); + + std::vector params; + std::shared_ptr ope; + auto has_instructions = false; + + if (is_macro) { + params = std::any_cast>(vs[2]); + ope = std::any_cast>(vs[4]); + if (vs.size() == 6) { has_instructions = true; } + } else { + ope = std::any_cast>(vs[3]); + if (vs.size() == 5) { has_instructions = true; } + } + + if (has_instructions) { + auto index = is_macro ? 5 : 4; + std::unordered_set types; + for (const auto &instruction : + std::any_cast>(vs[index])) { + const auto &type = instruction.type; + if (types.find(type) == types.end()) { + data.instructions[name].push_back(instruction); + types.insert(instruction.type); + if (type == "declare_symbol" || type == "check_symbol") { + if (!TokenChecker::is_token(*ope)) { ope = tok(ope); } + } + } else { + data.duplicates_of_instruction.emplace_back(type, + instruction.sv.data()); + } + } + } + + auto &grammar = *data.grammar; + if (!grammar.count(name)) { + auto &rule = grammar[name]; + rule <= ope; + rule.name = name; + rule.s_ = vs.sv().data(); + rule.line_ = line_info(vs.ss, rule.s_); + rule.ignoreSemanticValue = ignore; + rule.is_macro = is_macro; + rule.params = params; + + if (data.start.empty()) { + data.start = rule.name; + data.start_pos = rule.s_; + } + } else { + data.duplicates_of_definition.emplace_back(name, vs.sv().data()); + } + }; + + g["Definition"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_in_current_definition.clear(); + }; + + g["Expression"] = [&](const SemanticValues &vs) { + if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (auto i = 0u; i < vs.size(); i++) { + opes.emplace_back(std::any_cast>(vs[i])); + } + const std::shared_ptr ope = + std::make_shared(opes); + return ope; + } + }; + + g["Sequence"] = [&](const SemanticValues &vs) { + if (vs.empty()) { + return npd(lit("")); + } else if (vs.size() == 1) { + return std::any_cast>(vs[0]); + } else { + std::vector> opes; + for (const auto &x : vs) { + opes.emplace_back(std::any_cast>(x)); + } + const std::shared_ptr ope = std::make_shared(opes); + return ope; + } + }; + + g["Prefix"] = [&](const SemanticValues &vs) { + std::shared_ptr ope; + if (vs.size() == 1) { + ope = std::any_cast>(vs[0]); + } else { + assert(vs.size() == 2); + auto tok = std::any_cast(vs[0]); + ope = std::any_cast>(vs[1]); + if (tok == '&') { + ope = apd(ope); + } else { // '!' + ope = npd(ope); + } + } + return ope; + }; + + g["SuffixWithLabel"] = [&](const SemanticValues &vs, std::any &dt) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto &data = *std::any_cast(dt); + const auto &ident = std::any_cast(vs[1]); + auto label = ref(*data.grammar, ident, vs.sv().data(), false, {}); + auto recovery = rec(ref(*data.grammar, RECOVER_DEFINITION_NAME, + vs.sv().data(), true, {label})); + return cho4label_(ope, recovery); + } + }; + + struct Loop { + enum class Type { opt = 0, zom, oom, rep }; + Type type; + std::pair range; + }; + + g["Suffix"] = [&](const SemanticValues &vs) { + auto ope = std::any_cast>(vs[0]); + if (vs.size() == 1) { + return ope; + } else { + assert(vs.size() == 2); + auto loop = std::any_cast(vs[1]); + switch (loop.type) { + case Loop::Type::opt: return opt(ope); + case Loop::Type::zom: return zom(ope); + case Loop::Type::oom: return oom(ope); + default: // Regex-like repetition + return rep(ope, loop.range.first, loop.range.second); + } + } + }; + + g["Loop"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: // Option + return Loop{Loop::Type::opt, std::pair()}; + case 1: // Zero or More + return Loop{Loop::Type::zom, std::pair()}; + case 2: // One or More + return Loop{Loop::Type::oom, std::pair()}; + default: // Regex-like repetition + return Loop{Loop::Type::rep, + std::any_cast>(vs[0])}; + } + }; + + g["Primary"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + switch (vs.choice()) { + case 0: // Macro Reference + case 1: { // Reference + auto is_macro = vs.choice() == 0; + auto ignore = std::any_cast(vs[0]); + const auto &ident = std::any_cast(vs[1]); + + std::vector> args; + if (is_macro) { + args = std::any_cast>>(vs[2]); + } + + auto ope = ref(*data.grammar, ident, vs.sv().data(), is_macro, args); + if (ident == RECOVER_DEFINITION_NAME) { ope = rec(ope); } + + if (ignore) { + return ign(ope); + } else { + return ope; + } + } + case 2: { // (Expression) + return std::any_cast>(vs[0]); + } + case 3: { // TokenBoundary + return tok(std::any_cast>(vs[0])); + } + case 4: { // CaptureScope + return csc(std::any_cast>(vs[0])); + } + case 5: { // Capture + const auto &name = std::any_cast(vs[0]); + auto ope = std::any_cast>(vs[1]); + + data.captures_stack.back().insert(name); + data.captures_in_current_definition.insert(name); + + return cap(ope, [name](const char *a_s, size_t a_n, Context &c) { + c.capture_entries.emplace_back(name, std::string(a_s, a_n)); + }); + } + default: { + return std::any_cast>(vs[0]); + } + } + }; + + g["IdentCont"] = [](const SemanticValues &vs) { + return std::string(vs.sv().data(), vs.sv().length()); + }; + + g["Dictionary"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, false); + }; + g["DictionaryI"] = [](const SemanticValues &vs) { + auto items = vs.transform(); + return dic(items, true); + }; + + g["Literal"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return lit(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralI"] = [](const SemanticValues &vs) { + const auto &tok = vs.tokens.front(); + return liti(resolve_escape_sequence(tok.data(), tok.size())); + }; + g["LiteralD"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + g["LiteralID"] = [](const SemanticValues &vs) { + auto &tok = vs.tokens.front(); + return resolve_escape_sequence(tok.data(), tok.size()); + }; + + g["Class"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges); + }; + g["ClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return cls(ranges, true); + }; + g["NegatedClass"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges); + }; + g["NegatedClassI"] = [](const SemanticValues &vs) { + auto ranges = vs.transform>(); + return ncls(ranges, true); + }; + g["Range"] = [](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { + auto s1 = std::any_cast(vs[0]); + auto s2 = std::any_cast(vs[1]); + auto cp1 = decode_codepoint(s1.data(), s1.length()); + auto cp2 = decode_codepoint(s2.data(), s2.length()); + if (cp1 > cp2) { + throw SyntaxErrorException("characer range is out of order...", + vs.line_info()); + } + return std::pair(cp1, cp2); + } + case 1: { + auto s = std::any_cast(vs[0]); + auto cp = decode_codepoint(s.data(), s.length()); + return std::pair(cp, cp); + } + } + return std::pair(0, 0); + }; + g["Char"] = [](const SemanticValues &vs) { + return resolve_escape_sequence(vs.sv().data(), vs.sv().length()); + }; + + g["RepetitionRange"] = [&](const SemanticValues &vs) { + switch (vs.choice()) { + case 0: { // Number COMMA Number + auto min = std::any_cast(vs[0]); + auto max = std::any_cast(vs[1]); + return std::pair(min, max); + } + case 1: // Number COMMA + return std::pair(std::any_cast(vs[0]), + std::numeric_limits::max()); + case 2: { // Number + auto n = std::any_cast(vs[0]); + return std::pair(n, n); + } + default: // COMMA Number + return std::pair(std::numeric_limits::min(), + std::any_cast(vs[0])); + } + }; + g["Number"] = [&](const SemanticValues &vs) { + return vs.token_to_number(); + }; + + g["CapScope"].enter = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.emplace_back(); + }; + g["CapScope"].leave = [](const Context & /*c*/, const char * /*s*/, + size_t /*n*/, size_t /*matchlen*/, + std::any & /*value*/, std::any &dt) { + auto &data = *std::any_cast(dt); + data.captures_stack.pop_back(); + }; + + g["AND"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["NOT"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["QUESTION"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["STAR"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + g["PLUS"] = [](const SemanticValues &vs) { return *vs.sv().data(); }; + + g["DOT"] = [](const SemanticValues & /*vs*/) { return dot(); }; + + g["CUT"] = [](const SemanticValues & /*vs*/) { return cut(); }; + + g["BeginCap"] = [](const SemanticValues &vs) { return vs.token(); }; + + g["BackRef"] = [&](const SemanticValues &vs, std::any &dt) { + auto &data = *std::any_cast(dt); + + // Undefined back reference check + { + auto found = false; + auto it = data.captures_stack.rbegin(); + while (it != data.captures_stack.rend()) { + if (it->find(vs.token()) != it->end()) { + found = true; + break; + } + ++it; + } + if (!found) { + auto ptr = vs.token().data() - 1; // include '$' symbol + data.undefined_back_references.emplace_back(vs.token(), ptr); + } + } + + // NOTE: Disable packrat parsing if a back reference is not defined in + // captures in the current definition rule. + if (data.captures_in_current_definition.find(vs.token()) == + data.captures_in_current_definition.end()) { + data.enablePackratParsing = false; + } + + return bkr(vs.token_to_string()); + }; + + g["Ignore"] = [](const SemanticValues &vs) { return vs.size() > 0; }; + + g["Parameters"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + + g["Arguments"] = [](const SemanticValues &vs) { + return vs.transform>(); + }; + + g["PrecedenceClimbing"] = [](const SemanticValues &vs) { + PrecedenceClimbing::BinOpeInfo binOpeInfo; + size_t level = 1; + for (const auto &v : vs) { + auto tokens = std::any_cast>(v); + auto assoc = tokens[0][0]; + for (size_t i = 1; i < tokens.size(); i++) { + binOpeInfo[tokens[i]] = std::pair(level, assoc); + } + level++; + } + Instruction instruction; + instruction.type = "precedence"; + instruction.data = binOpeInfo; + instruction.sv = vs.sv(); + return instruction; + }; + g["PrecedenceInfo"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + g["PrecedenceOpe"] = [](const SemanticValues &vs) { return vs.token(); }; + g["PrecedenceAssoc"] = [](const SemanticValues &vs) { return vs.token(); }; + + g["ErrorMessage"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "error_message"; + instruction.data = std::any_cast(vs[0]); + instruction.sv = vs.sv(); + return instruction; + }; + + g["NoAstOpt"] = [](const SemanticValues &vs) { + Instruction instruction; + instruction.type = "no_ast_opt"; + instruction.sv = vs.sv(); + return instruction; + }; + + g["Instruction"] = [](const SemanticValues &vs) { + return vs.transform(); + }; + } + + bool apply_precedence_instruction(Definition &rule, + const PrecedenceClimbing::BinOpeInfo &info, + const char *s, Log log) { + try { + auto &seq = dynamic_cast(*rule.get_core_operator()); + auto atom = seq.opes_[0]; + auto &rep = dynamic_cast(*seq.opes_[1]); + auto &seq1 = dynamic_cast(*rep.ope_); + auto binop = seq1.opes_[0]; + auto atom1 = seq1.opes_[1]; + + auto atom_name = dynamic_cast(*atom).name_; + auto binop_name = dynamic_cast(*binop).name_; + auto atom1_name = dynamic_cast(*atom1).name_; + + if (!rep.is_zom() || atom_name != atom1_name || atom_name == binop_name) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); + } + return false; + } + + rule.holder_->ope_ = pre(atom, binop, info, rule); + rule.disable_action = true; + } catch (...) { + if (log) { + auto line = line_info(s, rule.s_); + log(line.first, line.second, + "'precedence' instruction cannot be applied to '" + rule.name + + "'.", + ""); + } + return false; + } + return true; + } + + ParserContext perform_core(const char *s, size_t n, const Rules &rules, + Log log, std::string requested_start, + bool enable_left_recursion = true) { + Data data; + auto &grammar = *data.grammar; + + // Built-in macros + { + // `%recover` + { + auto &rule = grammar[RECOVER_DEFINITION_NAME]; + rule <= ref(grammar, "x", "", false, {}); + rule.name = RECOVER_DEFINITION_NAME; + rule.s_ = "[native]"; + rule.ignoreSemanticValue = true; + rule.is_macro = true; + rule.params = {"x"}; + } + } + + try { + std::any dt = &data; + auto r = g["Grammar"].parse(s, n, dt, nullptr, log); + + if (!r.ret) { + if (log) { + if (r.error_info.message_pos) { + auto line = line_info(s, r.error_info.message_pos); + log(line.first, line.second, r.error_info.message, + r.error_info.label); + } else { + auto line = line_info(s, r.error_info.error_pos); + log(line.first, line.second, "syntax error", r.error_info.label); + } + } + return {}; + } + } catch (const SyntaxErrorException &e) { + if (log) { + auto line = e.line_info(); + log(line.first, line.second, e.what(), ""); + } + return {}; + } + + // User provided rules + for (auto [user_name, user_rule] : rules) { + auto name = user_name; + auto ignore = false; + if (!name.empty() && name[0] == '~') { + ignore = true; + name.erase(0, 1); + } + if (!name.empty()) { + auto &rule = grammar[name]; + rule <= user_rule; + rule.name = name; + rule.ignoreSemanticValue = ignore; + } + } + + // Check duplicated definitions + auto ret = true; + + if (!data.duplicates_of_definition.empty()) { + for (const auto &[name, ptr] : data.duplicates_of_definition) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the definition '" + name + "' is already defined.", ""); + } + } + ret = false; + } + + // Check duplicated instructions + if (!data.duplicates_of_instruction.empty()) { + for (const auto &[type, ptr] : data.duplicates_of_instruction) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the instruction '" + type + "' is already defined.", ""); + } + } + ret = false; + } + + // Check undefined back references + if (!data.undefined_back_references.empty()) { + for (const auto &[name, ptr] : data.undefined_back_references) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, + "the back reference '" + name + "' is undefined.", ""); + } + } + ret = false; + } + + // Set root definition + auto start = data.start; + + if (!requested_start.empty()) { + if (grammar.count(requested_start)) { + start = requested_start; + } else { + if (log) { + auto line = line_info(s, s); + log(line.first, line.second, + "the specified start rule '" + requested_start + + "' is undefined.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + auto &start_rule = grammar[start]; + + // Check if the start rule has ignore operator + { + if (start_rule.ignoreSemanticValue) { + if (log) { + auto line = line_info(s, start_rule.s_); + log(line.first, line.second, + "ignore operator cannot be applied to '" + start_rule.name + "'.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + + // Check missing definitions + auto referenced = std::unordered_set{ + WHITESPACE_DEFINITION_NAME, + WORD_DEFINITION_NAME, + RECOVER_DEFINITION_NAME, + start_rule.name, + }; + + for (auto &[_, rule] : grammar) { + ReferenceChecker vis(grammar, rule.params); + rule.accept(vis); + referenced.insert(vis.referenced.begin(), vis.referenced.end()); + for (const auto &[name, ptr] : vis.error_s) { + if (log) { + auto line = line_info(s, ptr); + log(line.first, line.second, vis.error_message[name], ""); + } + ret = false; + } + } + + for (auto &[name, rule] : grammar) { + if (!referenced.count(name)) { + if (log) { + auto line = line_info(s, rule.s_); + auto msg = "'" + name + "' is not referenced."; + log(line.first, line.second, msg, ""); + } + } + } + + if (!ret) { return {}; } + + // Link references + for (auto &x : grammar) { + auto &rule = x.second; + LinkReferences vis(grammar, rule.params); + rule.accept(vis); + } + + // Compute can_be_empty for each rule (fixed-point iteration) + { + bool changed = true; + while (changed) { + changed = false; + for (auto &[name, rule] : grammar) { + ComputeCanBeEmpty vis; + rule.accept(vis); + if (vis.result != rule.can_be_empty) { + rule.can_be_empty = vis.result; + changed = true; + } + } + } + } + + // Check left recursion + if (enable_left_recursion) { + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { rule.is_left_recursive = true; } + } + } else { + ret = true; + + for (auto &[name, rule] : grammar) { + DetectLeftRecursion vis(name); + rule.accept(vis); + if (vis.error_s) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, "'" + name + "' is left recursive.", + ""); + } + ret = false; + } + } + + if (!ret) { return {}; } + } + + // Check infinite loop + if (detect_infiniteLoop(data, start_rule, log, s)) { return {}; } + + // Automatic whitespace skipping + if (grammar.count(WHITESPACE_DEFINITION_NAME)) { + for (auto &x : grammar) { + auto &rule = x.second; + auto ope = rule.get_core_operator(); + if (IsLiteralToken::check(*ope)) { rule <= tok(ope); } + } + + auto &rule = grammar[WHITESPACE_DEFINITION_NAME]; + start_rule.whitespaceOpe = wsp(rule.get_core_operator()); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Word expression + if (grammar.count(WORD_DEFINITION_NAME)) { + auto &rule = grammar[WORD_DEFINITION_NAME]; + start_rule.wordOpe = rule.get_core_operator(); + + if (detect_infiniteLoop(data, rule, log, s)) { return {}; } + } + + // Apply instructions + for (const auto &[name, instructions] : data.instructions) { + auto &rule = grammar[name]; + + for (const auto &instruction : instructions) { + if (instruction.type == "precedence") { + const auto &info = + std::any_cast(instruction.data); + + if (!apply_precedence_instruction(rule, info, s, log)) { return {}; } + } else if (instruction.type == "error_message") { + rule.error_message = std::any_cast(instruction.data); + } else if (instruction.type == "no_ast_opt") { + rule.no_ast_opt = true; + } + } + } + + // Setup First-Set and ISpan optimizations + for (auto &x : grammar) { + SetupFirstSets vis; + x.second.accept(vis); + } + + return {data.grammar, start, data.enablePackratParsing}; + } + + bool detect_infiniteLoop(const Data &data, Definition &rule, const Log &log, + const char *s) const { + std::vector> refs; + std::unordered_map has_error_cache; + DetectInfiniteLoop vis(data.start_pos, rule.name, refs, has_error_cache); + rule.accept(vis); + if (vis.has_error) { + if (log) { + auto line = line_info(s, vis.error_s); + log(line.first, line.second, + "infinite loop is detected in '" + vis.error_name + "'.", ""); + } + return true; + } + return false; + } + + Grammar g; +}; + +/*----------------------------------------------------------------------------- + * AST + *---------------------------------------------------------------------------*/ + +template struct AstBase : public Annotation { + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::vector> &nodes, + size_t position = 0, size_t length = 0, size_t choice_count = 0, + size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(false), nodes(nodes) {} + + AstBase(const char *path, size_t line, size_t column, const char *name, + const std::string_view &token, size_t position = 0, size_t length = 0, + size_t choice_count = 0, size_t choice = 0) + : path(path ? path : ""), line(line), column(column), name(name), + position(position), length(length), choice_count(choice_count), + choice(choice), original_name(name), + original_choice_count(choice_count), original_choice(choice), + tag(str2tag(name)), original_tag(tag), is_token(true), token(token) {} + + AstBase(const AstBase &ast, const char *original_name, size_t position = 0, + size_t length = 0, size_t original_choice_count = 0, + size_t original_choice = 0) + : path(ast.path), line(ast.line), column(ast.column), name(ast.name), + position(position), length(length), choice_count(ast.choice_count), + choice(ast.choice), original_name(original_name), + original_choice_count(original_choice_count), + original_choice(original_choice), tag(ast.tag), + original_tag(str2tag(original_name)), is_token(ast.is_token), + token(ast.token), nodes(ast.nodes), parent(ast.parent) {} + + const std::string path; + const size_t line = 1; + const size_t column = 1; + + const std::string name; + size_t position; + size_t length; + const size_t choice_count; + const size_t choice; + const std::string original_name; + const size_t original_choice_count; + const size_t original_choice; + const unsigned int tag; + const unsigned int original_tag; + + const bool is_token; + const std::string_view token; + + std::vector>> nodes; + std::weak_ptr> parent; + + std::string token_to_string() const { + assert(is_token); + return std::string(token); + } + + template T token_to_number() const { + return token_to_number_(token); + } +}; + +template +void ast_to_s_core(const std::shared_ptr &ptr, std::string &s, int level, + std::function fn) { + const auto &ast = *ptr; + for (auto i = 0; i < level; i++) { + s += " "; + } + auto name = ast.original_name; + if (ast.original_choice_count > 0) { + name += "/" + std::to_string(ast.original_choice); + } + if (ast.name != ast.original_name) { name += "[" + ast.name + "]"; } + if (ast.is_token) { + s += "- " + name + " ("; + s += ast.token; + s += ")\n"; + } else { + s += "+ " + name + "\n"; + } + if (fn) { s += fn(ast, level + 1); } + for (const auto &node : ast.nodes) { + ast_to_s_core(node, s, level + 1, fn); + } +} + +template +std::string +ast_to_s(const std::shared_ptr &ptr, + std::function fn = nullptr) { + std::string s; + ast_to_s_core(ptr, s, 0, fn); + return s; +} + +struct AstOptimizer { + AstOptimizer(bool mode, const std::vector &rules = {}) + : mode_(mode), rules_(rules) {} + + template + std::shared_ptr optimize(std::shared_ptr original, + std::shared_ptr parent = nullptr) { + auto found = + std::find(rules_.begin(), rules_.end(), original->name) != rules_.end(); + auto opt = mode_ ? !found : found; + + if (opt && original->nodes.size() == 1) { + auto child = optimize(original->nodes[0], parent); + auto ast = std::make_shared(*child, original->name.data(), + original->position, original->length, + original->choice_count, original->choice); + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; + } + + auto ast = std::make_shared(*original); + ast->parent = parent; + ast->nodes.clear(); + for (const auto &node : original->nodes) { + auto child = optimize(node, ast); + ast->nodes.push_back(child); + } + return ast; + } + +private: + const bool mode_; + const std::vector rules_; +}; + +struct EmptyType {}; +using Ast = AstBase; + +template void add_ast_action(Definition &rule) { + rule.action = [&](const SemanticValues &vs) { + auto line = vs.line_info(); + + if (rule.is_token()) { + return std::make_shared( + vs.path, line.first, line.second, rule.name.data(), vs.token(), + std::distance(vs.ss, vs.sv().data()), vs.sv().length(), + vs.choice_count(), vs.choice()); + } + + auto ast = + std::make_shared(vs.path, line.first, line.second, rule.name.data(), + vs.transform>(), + std::distance(vs.ss, vs.sv().data()), + vs.sv().length(), vs.choice_count(), vs.choice()); + + for (auto &node : ast->nodes) { + node->parent = ast; + } + return ast; + }; +} + +#define PEG_EXPAND(...) __VA_ARGS__ +#define PEG_CONCAT(a, b) a##b +#define PEG_CONCAT2(a, b) PEG_CONCAT(a, b) + +#define PEG_PICK( \ + a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, \ + a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, \ + a32, a33, a34, a35, a36, a37, a38, a39, a40, a41, a42, a43, a44, a45, a46, \ + a47, a48, a49, a50, a51, a52, a53, a54, a55, a56, a57, a58, a59, a60, a61, \ + a62, a63, a64, a65, a66, a67, a68, a69, a70, a71, a72, a73, a74, a75, a76, \ + a77, a78, a79, a80, a81, a82, a83, a84, a85, a86, a87, a88, a89, a90, a91, \ + a92, a93, a94, a95, a96, a97, a98, a99, a100, ...) \ + a100 + +#define PEG_COUNT(...) \ + PEG_EXPAND(PEG_PICK( \ + __VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, \ + 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, \ + 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, \ + 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, \ + 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, \ + 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)) + +#define PEG_DEF_1(r) \ + peg::Definition r; \ + r.name = #r; \ + peg::add_ast_action(r); + +#define PEG_DEF_2(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_1(__VA_ARGS__)) +#define PEG_DEF_3(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_2(__VA_ARGS__)) +#define PEG_DEF_4(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_3(__VA_ARGS__)) +#define PEG_DEF_5(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_4(__VA_ARGS__)) +#define PEG_DEF_6(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_5(__VA_ARGS__)) +#define PEG_DEF_7(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_6(__VA_ARGS__)) +#define PEG_DEF_8(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_7(__VA_ARGS__)) +#define PEG_DEF_9(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_8(__VA_ARGS__)) +#define PEG_DEF_10(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_9(__VA_ARGS__)) +#define PEG_DEF_11(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_10(__VA_ARGS__)) +#define PEG_DEF_12(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_11(__VA_ARGS__)) +#define PEG_DEF_13(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_12(__VA_ARGS__)) +#define PEG_DEF_14(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_13(__VA_ARGS__)) +#define PEG_DEF_15(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_14(__VA_ARGS__)) +#define PEG_DEF_16(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_15(__VA_ARGS__)) +#define PEG_DEF_17(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_16(__VA_ARGS__)) +#define PEG_DEF_18(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_17(__VA_ARGS__)) +#define PEG_DEF_19(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_18(__VA_ARGS__)) +#define PEG_DEF_20(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_19(__VA_ARGS__)) +#define PEG_DEF_21(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_20(__VA_ARGS__)) +#define PEG_DEF_22(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_21(__VA_ARGS__)) +#define PEG_DEF_23(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_22(__VA_ARGS__)) +#define PEG_DEF_24(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_23(__VA_ARGS__)) +#define PEG_DEF_25(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_24(__VA_ARGS__)) +#define PEG_DEF_26(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_25(__VA_ARGS__)) +#define PEG_DEF_27(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_26(__VA_ARGS__)) +#define PEG_DEF_28(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_27(__VA_ARGS__)) +#define PEG_DEF_29(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_28(__VA_ARGS__)) +#define PEG_DEF_30(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_29(__VA_ARGS__)) +#define PEG_DEF_31(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_30(__VA_ARGS__)) +#define PEG_DEF_32(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_31(__VA_ARGS__)) +#define PEG_DEF_33(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_32(__VA_ARGS__)) +#define PEG_DEF_34(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_33(__VA_ARGS__)) +#define PEG_DEF_35(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_34(__VA_ARGS__)) +#define PEG_DEF_36(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_35(__VA_ARGS__)) +#define PEG_DEF_37(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_36(__VA_ARGS__)) +#define PEG_DEF_38(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_37(__VA_ARGS__)) +#define PEG_DEF_39(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_38(__VA_ARGS__)) +#define PEG_DEF_40(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_39(__VA_ARGS__)) +#define PEG_DEF_41(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_40(__VA_ARGS__)) +#define PEG_DEF_42(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_41(__VA_ARGS__)) +#define PEG_DEF_43(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_42(__VA_ARGS__)) +#define PEG_DEF_44(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_43(__VA_ARGS__)) +#define PEG_DEF_45(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_44(__VA_ARGS__)) +#define PEG_DEF_46(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_45(__VA_ARGS__)) +#define PEG_DEF_47(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_46(__VA_ARGS__)) +#define PEG_DEF_48(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_47(__VA_ARGS__)) +#define PEG_DEF_49(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_48(__VA_ARGS__)) +#define PEG_DEF_50(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_49(__VA_ARGS__)) +#define PEG_DEF_51(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_50(__VA_ARGS__)) +#define PEG_DEF_52(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_51(__VA_ARGS__)) +#define PEG_DEF_53(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_52(__VA_ARGS__)) +#define PEG_DEF_54(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_53(__VA_ARGS__)) +#define PEG_DEF_55(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_54(__VA_ARGS__)) +#define PEG_DEF_56(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_55(__VA_ARGS__)) +#define PEG_DEF_57(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_56(__VA_ARGS__)) +#define PEG_DEF_58(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_57(__VA_ARGS__)) +#define PEG_DEF_59(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_58(__VA_ARGS__)) +#define PEG_DEF_60(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_59(__VA_ARGS__)) +#define PEG_DEF_61(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_60(__VA_ARGS__)) +#define PEG_DEF_62(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_61(__VA_ARGS__)) +#define PEG_DEF_63(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_62(__VA_ARGS__)) +#define PEG_DEF_64(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_63(__VA_ARGS__)) +#define PEG_DEF_65(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_64(__VA_ARGS__)) +#define PEG_DEF_66(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_65(__VA_ARGS__)) +#define PEG_DEF_67(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_66(__VA_ARGS__)) +#define PEG_DEF_68(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_67(__VA_ARGS__)) +#define PEG_DEF_69(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_68(__VA_ARGS__)) +#define PEG_DEF_70(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_69(__VA_ARGS__)) +#define PEG_DEF_71(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_70(__VA_ARGS__)) +#define PEG_DEF_72(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_71(__VA_ARGS__)) +#define PEG_DEF_73(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_72(__VA_ARGS__)) +#define PEG_DEF_74(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_73(__VA_ARGS__)) +#define PEG_DEF_75(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_74(__VA_ARGS__)) +#define PEG_DEF_76(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_75(__VA_ARGS__)) +#define PEG_DEF_77(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_76(__VA_ARGS__)) +#define PEG_DEF_78(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_77(__VA_ARGS__)) +#define PEG_DEF_79(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_78(__VA_ARGS__)) +#define PEG_DEF_80(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_79(__VA_ARGS__)) +#define PEG_DEF_81(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_80(__VA_ARGS__)) +#define PEG_DEF_82(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_81(__VA_ARGS__)) +#define PEG_DEF_83(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_82(__VA_ARGS__)) +#define PEG_DEF_84(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_83(__VA_ARGS__)) +#define PEG_DEF_85(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_84(__VA_ARGS__)) +#define PEG_DEF_86(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_85(__VA_ARGS__)) +#define PEG_DEF_87(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_86(__VA_ARGS__)) +#define PEG_DEF_88(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_87(__VA_ARGS__)) +#define PEG_DEF_89(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_88(__VA_ARGS__)) +#define PEG_DEF_90(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_89(__VA_ARGS__)) +#define PEG_DEF_91(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_90(__VA_ARGS__)) +#define PEG_DEF_92(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_91(__VA_ARGS__)) +#define PEG_DEF_93(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_92(__VA_ARGS__)) +#define PEG_DEF_94(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_93(__VA_ARGS__)) +#define PEG_DEF_95(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_94(__VA_ARGS__)) +#define PEG_DEF_96(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_95(__VA_ARGS__)) +#define PEG_DEF_97(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_96(__VA_ARGS__)) +#define PEG_DEF_98(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_97(__VA_ARGS__)) +#define PEG_DEF_99(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_98(__VA_ARGS__)) +#define PEG_DEF_100(r1, ...) PEG_EXPAND(PEG_DEF_1(r1) PEG_DEF_99(__VA_ARGS__)) + +#define AST_DEFINITIONS(...) \ + PEG_EXPAND(PEG_CONCAT2(PEG_DEF_, PEG_COUNT(__VA_ARGS__))(__VA_ARGS__)) + +/*----------------------------------------------------------------------------- + * parser + *---------------------------------------------------------------------------*/ + +class parser { +public: + parser() = default; + + parser(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + load_grammar(s, n, rules, start); + } + + parser(const char *s, size_t n, std::string_view start = {}) + : parser(s, n, Rules(), start) {} + + parser(std::string_view sv, const Rules &rules, std::string_view start = {}) + : parser(sv.data(), sv.size(), rules, start) {} + + parser(std::string_view sv, std::string_view start = {}) + : parser(sv.data(), sv.size(), Rules(), start) {} + +#if defined(__cpp_lib_char8_t) + parser(std::u8string_view sv, const Rules &rules, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), rules, + start) {} + + parser(std::u8string_view sv, std::string_view start = {}) + : parser(reinterpret_cast(sv.data()), sv.size(), Rules(), + start) {} +#endif + + operator bool() const { return grammar_ != nullptr; } + + bool load_grammar(const char *s, size_t n, const Rules &rules, + std::string_view start = {}) { + auto cxt = + ParserGenerator::parse(s, n, rules, log_, start, enableLeftRecursion_); + grammar_ = cxt.grammar; + start_ = cxt.start; + enablePackratParsing_ = cxt.enablePackratParsing; + return grammar_ != nullptr; + } + + bool load_grammar(const char *s, size_t n, std::string_view start = {}) { + return load_grammar(s, n, Rules(), start); + } + + bool load_grammar(std::string_view sv, const Rules &rules, + std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), rules, start); + } + + bool load_grammar(std::string_view sv, std::string_view start = {}) { + return load_grammar(sv.data(), sv.size(), Rules(), start); + } + + bool parse_n(const char *s, size_t n, const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, path, log_); + return post_process(s, n, result); + } + return false; + } + + bool parse_n(const char *s, size_t n, std::any &dt, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse(s, n, dt, path, log_); + return post_process(s, n, result); + } + return false; + } + + template + bool parse_n(const char *s, size_t n, T &val, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, val, path, log_); + return post_process(s, n, result); + } + return false; + } + + template + bool parse_n(const char *s, size_t n, std::any &dt, T &val, + const char *path = nullptr) const { + if (grammar_ != nullptr) { + const auto &rule = (*grammar_)[start_]; + auto result = rule.parse_and_get_value(s, n, dt, val, path, log_); + return post_process(s, n, result); + } + return false; + } + + bool parse(std::string_view sv, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), path); + } + + bool parse(std::string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, path); + } + + template + bool parse(std::string_view sv, T &val, const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), val, path); + } + + template + bool parse(std::string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(sv.data(), sv.size(), dt, val, path); + } + +#if defined(__cpp_lib_char8_t) + bool parse(std::u8string_view sv, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), path); + } + + bool parse(std::u8string_view sv, std::any &dt, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + path); + } + + template + bool parse(std::u8string_view sv, T &val, const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), val, + path); + } + + template + bool parse(std::u8string_view sv, std::any &dt, T &val, + const char *path = nullptr) const { + return parse_n(reinterpret_cast(sv.data()), sv.size(), dt, + val, path); + } +#endif + + Definition &operator[](const char *s) { return (*grammar_)[s]; } + + const Definition &operator[](const char *s) const { return (*grammar_)[s]; } + + const Grammar &get_grammar() const { return *grammar_; } + + void disable_eoi_check() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.eoi_check = false; + } + } + + void enable_left_recursion(bool enable = true) { + enableLeftRecursion_ = enable; + } + + void enable_packrat_parsing() { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.enablePackratParsing = enablePackratParsing_; + } + } + + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; + } + } + + void enable_trace(TracerEnter tracer_enter, TracerLeave tracer_leave, + TracerStartOrEnd tracer_start, + TracerStartOrEnd tracer_end) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.tracer_enter = tracer_enter; + rule.tracer_leave = tracer_leave; + rule.tracer_start = tracer_start; + rule.tracer_end = tracer_end; + } + } + + void set_verbose_trace(bool verbose_trace) { + if (grammar_ != nullptr) { + auto &rule = (*grammar_)[start_]; + rule.verbose_trace = verbose_trace; + } + } + + template parser &enable_ast() { + for (auto &[_, rule] : *grammar_) { + if (!rule.action) { add_ast_action(rule); } + } + return *this; + } + + template + std::shared_ptr optimize_ast(std::shared_ptr ast, + bool opt_mode = true) const { + return AstOptimizer(opt_mode, get_no_ast_opt_rules()).optimize(ast); + } + + void set_logger(Log log) { log_ = log; } + + void set_logger( + std::function + log) { + log_ = [log](size_t line, size_t col, const std::string &msg, + const std::string & /*rule*/) { log(line, col, msg); }; + } + +private: + bool post_process(const char *s, size_t n, Definition::Result &r) const { + if (log_ && !r.ret) { r.error_info.output_log(log_, s, n); } + return r.ret && !r.recovered; + } + + std::vector get_no_ast_opt_rules() const { + std::vector rules; + for (auto &[name, rule] : *grammar_) { + if (rule.no_ast_opt) { rules.push_back(name); } + } + return rules; + } + + std::shared_ptr grammar_; + std::string start_; + bool enableLeftRecursion_ = true; + bool enablePackratParsing_ = false; + Log log_; +}; + +/*----------------------------------------------------------------------------- + * enable_tracing + *---------------------------------------------------------------------------*/ + +inline void enable_tracing(parser &parser, std::ostream &os) { + parser.enable_trace( + [&](auto &ope, auto s, auto, auto &, auto &c, auto &, auto &trace_data) { + auto prev_pos = std::any_cast(trace_data); + auto pos = static_cast(s - c.s); + auto backtrack = (pos < prev_pos ? "*" : ""); + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + std::string name; + { + name = peg::TraceOpeName::get(const_cast(ope)); + + auto lit = dynamic_cast(&ope); + if (lit) { name += " '" + peg::escape_characters(lit->lit_) + "'"; } + } + os << "E " << pos + 1 << backtrack << "\t" << indent << "┌" << name + << " #" << c.trace_ids.back() << std::endl; + trace_data = static_cast(pos); + }, + [&](auto &ope, auto s, auto, auto &sv, auto &c, auto &, auto len, + auto &) { + auto pos = static_cast(s - c.s); + if (len != static_cast(-1)) { pos += len; } + std::string indent; + auto level = c.trace_ids.size() - 1; + while (level--) { + indent += "│"; + } + auto ret = len != static_cast(-1) ? "└o " : "└x "; + auto name = peg::TraceOpeName::get(const_cast(ope)); + std::stringstream choice; + if (sv.choice_count() > 0) { + choice << " " << sv.choice() << "/" << sv.choice_count(); + } + std::string token; + if (!sv.tokens.empty()) { + token += ", token '"; + token += sv.tokens[0]; + token += "'"; + } + std::string matched; + if (peg::success(len) && + peg::TokenChecker::is_token(const_cast(ope))) { + matched = ", match '" + peg::escape_characters(s, len) + "'"; + } + os << "L " << pos + 1 << "\t" << indent << ret << name << " #" + << c.trace_ids.back() << choice.str() << token << matched + << std::endl; + }, + [&](auto &trace_data) { trace_data = static_cast(0); }, + [&](auto &) {}); +} + +/*----------------------------------------------------------------------------- + * enable_profiling + *---------------------------------------------------------------------------*/ + +inline void enable_profiling(parser &parser, std::ostream &os) { + struct Stats { + struct Item { + std::string name; + size_t success; + size_t fail; + }; + std::vector items; + std::map index; + size_t total = 0; + std::chrono::steady_clock::time_point start; + }; + + parser.enable_trace( + [&](auto &ope, auto, auto, auto &, auto &, auto &, std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); + + auto &name = holder->name(); + if (stats.index.find(name) == stats.index.end()) { + stats.index[name] = stats.index.size(); + stats.items.push_back({name, 0, 0}); + } + stats.total++; + } + }, + [&](auto &ope, auto, auto, auto &, auto &, auto &, auto len, + std::any &trace_data) { + if (auto holder = dynamic_cast(&ope)) { + auto &stats = *std::any_cast(trace_data); + + auto &name = holder->name(); + auto index = stats.index[name]; + auto &stat = stats.items[index]; + if (len != static_cast(-1)) { + stat.success++; + } else { + stat.fail++; + } + + if (index == 0) { + auto end = std::chrono::steady_clock::now(); + auto nano = std::chrono::duration_cast( + end - stats.start) + .count(); + auto sec = nano / 1000000.0; + os << "duration: " << sec << "s (" << nano << "µs)" << std::endl + << std::endl; + + char buff[BUFSIZ]; + size_t total_success = 0; + size_t total_fail = 0; + for (auto &[name, success, fail] : stats.items) { + total_success += success; + total_fail += fail; + } + + os << " id total % success fail " + "definition" + << std::endl; + + auto grand_total = total_success + total_fail; + snprintf(buff, BUFSIZ, "%4s %10zu %5s %10zu %10zu %s", "", + grand_total, "", total_success, total_fail, + "Total counters"); + os << buff << std::endl; + + snprintf(buff, BUFSIZ, "%4s %10s %5s %10.2f %10.2f %s", "", "", + "", total_success * 100.0 / grand_total, + total_fail * 100.0 / grand_total, "% success/fail"); + os << buff << std::endl << std::endl; + ; + + size_t id = 0; + for (auto &[name, success, fail] : stats.items) { + auto total = success + fail; + auto ratio = total * 100.0 / stats.total; + snprintf(buff, BUFSIZ, "%4zu %10zu %5.2f %10zu %10zu %s", id, + total, ratio, success, fail, name.c_str()); + os << buff << std::endl; + id++; + } + } + } + }, + [&](auto &trace_data) { + auto stats = new Stats{}; + stats->start = std::chrono::steady_clock::now(); + trace_data = stats; + }, + [&](auto &trace_data) { + auto stats = std::any_cast(trace_data); + delete stats; + }); +} +} // namespace peg diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h new file mode 100644 index 000000000..606947143 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -0,0 +1,34 @@ +#ifndef COCKATRICE_QT_UTILS_H +#define COCKATRICE_QT_UTILS_H +#include + +namespace QtUtils +{ +template T *findParentOfType(const QObject *obj) +{ + const QObject *p = obj ? obj->parent() : nullptr; + while (p) { + if (auto casted = qobject_cast(const_cast(p))) { + return casted; + } + p = p->parent(); + } + return nullptr; +} + +static inline void clearLayoutRec(QLayout *l) +{ + if (!l) + return; + QLayoutItem *it; + while ((it = l->takeAt(0)) != nullptr) { + if (QWidget *w = it->widget()) + w->deleteLater(); + if (QLayout *sub = it->layout()) + clearLayoutRec(sub); + delete it; + } +} +} // namespace QtUtils + +#endif // COCKATRICE_QT_UTILS_H diff --git a/common/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h similarity index 100% rename from common/trice_limits.h rename to libcockatrice_utility/libcockatrice/utility/trice_limits.h diff --git a/libcockatrice_utility/libcockatrice/utility/zone_names.h b/libcockatrice_utility/libcockatrice/utility/zone_names.h new file mode 100644 index 000000000..d1463de6a --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/zone_names.h @@ -0,0 +1,19 @@ +#ifndef ZONE_NAMES_H +#define ZONE_NAMES_H + +namespace ZoneNames +{ +// Protocol-level zone identifiers shared between client and server. +// These must match exactly across all components. + +constexpr const char *TABLE = "table"; +constexpr const char *GRAVE = "grave"; +constexpr const char *EXILE = "rfg"; // "removed from game" +constexpr const char *HAND = "hand"; +constexpr const char *DECK = "deck"; +constexpr const char *SIDEBOARD = "sb"; +constexpr const char *STACK = "stack"; + +} // namespace ZoneNames + +#endif // ZONE_NAMES_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 5a76f3274..3bb4de5df 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -1,47 +1,42 @@ -# CMakeLists for oracle directory -# -# provides the oracle binary - +cmake_minimum_required(VERSION 3.16) project(Oracle VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") -# paths +# ------------------------ +# Paths and directories +# ------------------------ set(DESKTOPDIR share/applications CACHE STRING "path to .desktop files" ) +set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") +set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") +set(ORACLE_WIN32_QM_INSTALL_DIR "translations") + +# ------------------------ +# Sources +# ------------------------ set(oracle_SOURCES src/main.cpp src/oraclewizard.cpp src/oracleimporter.cpp + src/pages.cpp src/pagetemplates.cpp src/parsehelpers.cpp src/qt-json/json.cpp - ../cockatrice/src/game/cards/card_info.cpp - ../cockatrice/src/client/ui/widgets/quick_settings/settings_button_widget.cpp - ../cockatrice/src/client/ui/widgets/quick_settings/settings_popup_widget.cpp - ../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp - ../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp - ../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp - ../cockatrice/src/settings/cache_settings.cpp - ../cockatrice/src/settings/shortcuts_settings.cpp - ../cockatrice/src/settings/card_counter_settings.cpp - ../cockatrice/src/settings/card_database_settings.cpp - ../cockatrice/src/settings/servers_settings.cpp - ../cockatrice/src/settings/settings_manager.cpp - ../cockatrice/src/settings/message_settings.cpp - ../cockatrice/src/settings/recents_settings.cpp - ../cockatrice/src/settings/game_filters_settings.cpp - ../cockatrice/src/settings/layouts_settings.cpp - ../cockatrice/src/settings/download_settings.cpp - ../cockatrice/src/settings/card_override_settings.cpp - ../cockatrice/src/settings/debug_settings.cpp - ../cockatrice/src/client/ui/theme_manager.cpp - ../cockatrice/src/client/network/release_channel.cpp + ../cockatrice/src/client/settings/cache_settings.cpp + ../cockatrice/src/client/settings/card_counter_settings.cpp + ../cockatrice/src/client/settings/shortcuts_settings.cpp + ../cockatrice/src/client/network/update/client/release_channel.cpp + ../cockatrice/src/interface/theme_manager.cpp + ../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp + ../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp ${VERSION_STRING_CPP} ) -set(oracle_RESOURCES oracle.qrc) +# ------------------------ +# Translations +# ------------------------ if(UPDATE_TRANSLATIONS) file(GLOB_RECURSE translate_oracle_SRCS src/*.cpp src/*.h ../cockatrice/src/settingscache.cpp) @@ -63,37 +58,49 @@ if(APPLE) set(oracle_SOURCES ${oracle_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns) endif(APPLE) +set(oracle_RESOURCES oracle.qrc) + +# ------------------------ +# Qt resources +# ------------------------ if(Qt6_FOUND) qt6_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) elseif(Qt5_FOUND) qt5_add_resources(oracle_RESOURCES_RCC ${oracle_RESOURCES}) endif() +# ------------------------ +# Include directories +# ------------------------ include_directories(../cockatrice/src) -include_directories(../common) -# Libz is required to support zipped files +# ------------------------ +# Optional libraries +# ------------------------ +# ZLIB find_package(ZLIB) if(ZLIB_FOUND) include_directories(${ZLIB_INCLUDE_DIRS}) add_definitions("-DHAS_ZLIB") - - set(oracle_SOURCES ${oracle_SOURCES} src/zip/unzip.cpp src/zip/zipglobal.cpp) + list(APPEND oracle_SOURCES src/zip/unzip.cpp src/zip/zipglobal.cpp) else() message(STATUS "Oracle: zlib not found; ZIP support disabled") endif() -# LibLZMA is required to support xz files +# LZMA find_package(LibLZMA) if(LIBLZMA_FOUND) include_directories(${LIBLZMA_INCLUDE_DIRS}) add_definitions("-DHAS_LZMA") - - set(oracle_SOURCES ${oracle_SOURCES} src/lzma/decompress.cpp) + list(APPEND oracle_SOURCES src/lzma/decompress.cpp) else() message(STATUS "Oracle: LibLZMA not found; xz support disabled") endif() +# ------------------------ +# Build executable +# ------------------------ + set(ORACLE_MAC_QM_INSTALL_DIR "oracle.app/Contents/Resources/translations") set(ORACLE_UNIX_QM_INSTALL_DIR "share/oracle/translations") set(ORACLE_WIN32_QM_INSTALL_DIR "translations") @@ -130,7 +137,16 @@ elseif(Qt5_FOUND) endif() endif() -target_link_libraries(oracle PUBLIC ${ORACLE_QT_MODULES}) +# ------------------------ +# Link libraries +# ------------------------ +target_link_libraries( + oracle + PUBLIC libcockatrice_card + PUBLIC libcockatrice_settings + PUBLIC libcockatrice_network + PUBLIC ${ORACLE_QT_MODULES} +) if(ZLIB_FOUND) target_link_libraries(oracle PUBLIC ${ZLIB_LIBRARIES}) @@ -140,6 +156,9 @@ if(LIBLZMA_FOUND) target_link_libraries(oracle PUBLIC ${LIBLZMA_LIBRARIES}) endif() +# ------------------------ +# Install rules +# ------------------------ if(UNIX) if(APPLE) set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}") @@ -165,6 +184,9 @@ if(NOT WIN32 AND NOT APPLE) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/oracle.desktop DESTINATION ${DESKTOPDIR}) endif(NOT WIN32 AND NOT APPLE) +# ------------------------ +# Qt plugin handling +# ------------------------ if(APPLE) # these needs to be relative to CMAKE_INSTALL_PREFIX set(plugin_dest_dir oracle.app/Contents/Plugins) @@ -259,6 +281,9 @@ Translations = Resources/translations\") ) endif() +# ------------------------ +# Qt translations +# ------------------------ if(Qt6_FOUND AND Qt6LinguistTools_FOUND) #Qt6 Translations happen after the executable is built up if(UPDATE_TRANSLATIONS) diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts index e0fd59842..943b44a97 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -4,22 +4,22 @@ IntroPage - + Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -27,134 +27,134 @@ LoadSetsPage - + Source selection - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: - + Local file: - + Restore default URL - + Choose file... - + Load sets file - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error - + The provided URL is not valid. - + Downloading (0MB) - + Please choose a file. - + Cannot open file '%1'. - + Downloading (%1MB) - + Network error: %1. - + Parsing file - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. @@ -162,42 +162,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -205,42 +220,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -248,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -256,7 +286,7 @@ OracleWizard - + Oracle Importer @@ -264,22 +294,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -287,73 +317,73 @@ SaveSetsPage - - + + Error - + No set has been imported. - + Sets imported - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + The file could not be saved to %1 @@ -361,34 +391,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -537,7 +589,7 @@ i18n - + English diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index 19007a3ca..5def0c887 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -1,9 +1,9 @@ #include "main.h" -#include "client/ui/theme_manager.h" +#include "interface/theme_manager.h" #include "oraclewizard.h" -#include "settings/cache_settings.h" +#include <../../cockatrice/src/client/settings/cache_settings.h> #include #include #include diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 0fd2beaa7..578afd98d 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -1,6 +1,7 @@ #include "oracleimporter.h" -#include "game/cards/card_database_parser/cockatrice_xml_4.h" +#include "libcockatrice/interfaces/noop_card_preference_provider.h" +#include "libcockatrice/interfaces/noop_card_set_priority_controller.h" #include "parsehelpers.h" #include "qt-json/json.h" @@ -8,6 +9,12 @@ #include #include #include +#include +#include + +static const QList kConstructedCounts = {{4, "legal"}, {0, "banned"}}; + +static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"}}; SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, @@ -37,8 +44,6 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s bool OracleImporter::readSetsFromByteArray(const QByteArray &data) { - QList newSetList; - bool ok; auto setsMap = QtJson::Json::parse(QString(data), ok).toMap().value("data").toMap(); if (!ok) { @@ -46,6 +51,8 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) return false; } + QList newSetList; + QListIterator it(setsMap.values()); while (it.hasNext()) { @@ -149,7 +156,8 @@ CardInfoPtr OracleImporter::addCard(QString name, QStringList symbols = manacost.split("}"); QString formattedCardCost; for (QString symbol : symbols) { - if (symbol.contains(QRegularExpression("[0-9WUBGRP]/[0-9WUBGRP]"))) { + static const auto manaCostPattern = QRegularExpression("[0-9WUBGRP]/[0-9WUBGRP]"); + if (symbol.contains(manaCostPattern)) { symbol.append("}"); } else { symbol.remove(QChar('{')); @@ -173,19 +181,19 @@ CardInfoPtr OracleImporter::addCard(QString name, // DETECT CARD POSITIONING INFO - // cards that enter the field tapped - bool cipt = parseCipt(name, text); - bool landscapeOrientation = properties.value("maintype").toString() == "Battle" || properties.value("layout").toString() == "split" || properties.value("layout").toString() == "planar"; + // cards that enter the field tapped + bool cipt = parseCipt(name, text) || landscapeOrientation; + // table row int tableRow = 1; QString mainCardType = properties.value("maintype").toString(); - if ((mainCardType == "Land")) + if (mainCardType == "Land") tableRow = 0; - else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) + else if (mainCardType == "Sorcery" || mainCardType == "Instant") tableRow = 3; else if (mainCardType == "Creature") tableRow = 2; @@ -199,11 +207,11 @@ CardInfoPtr OracleImporter::addCard(QString name, bool upsideDown = layout == "flip" && side == "back"; // insert the card and its properties - QList reverseRelatedCards; SetToPrintingsMap setsInfo; setsInfo[printingInfo.getSet()->getShortName()].append(printingInfo); - CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, - setsInfo, cipt, landscapeOrientation, tableRow, upsideDown); + CardInfo::UiAttributes attributes = {cipt, landscapeOrientation, tableRow, upsideDown}; + CardInfoPtr newCard = + CardInfo::newInstance(name, text, isToken, properties, relatedCards, {}, setsInfo, attributes); if (name.isEmpty()) { qDebug() << "warning: an empty card was added to set" << printingInfo.getSet()->getShortName(); @@ -234,26 +242,26 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList // mtgjson name => xml name static const QMap identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}}; - int numCards = 0; - QMap, QString>> splitCards; - QString ptSeparator("/"); - QVariantMap card; - QString layout, name, text, colors, colorIdentity, faceName; + static const QString ptSeparator = "/"; static constexpr bool isToken = false; static const QList setsWithCardsWithSameNameButDifferentText = {"UST"}; - QVariantHash properties; - PrintingInfo printingInfo; - QList relatedCards; + + int numCards = 0; + + // Keeps track of any split card faces encountered so far + QMap, QString>> splitCards; + + // Keeps track of all names encountered so far QList allNameProps; for (const QVariant &cardVar : cardsList) { - card = cardVar.toMap(); + QVariantMap card = cardVar.toMap(); /* Currently used layouts are: * augment, double_faced_token, flip, host, leveler, meld, normal, planar, * saga, scheme, split, token, transform, vanguard */ - layout = getStringPropertyFromMap(card, "layout"); + QString layout = getStringPropertyFromMap(card, "layout"); // don't import tokens from the json file if (layout == "token") { @@ -261,44 +269,45 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } // normal cards handling - name = getStringPropertyFromMap(card, "name"); - text = getStringPropertyFromMap(card, "text"); - faceName = getStringPropertyFromMap(card, "faceName"); + QString name = getStringPropertyFromMap(card, "name"); + QString text = getStringPropertyFromMap(card, "text"); + QString faceName = getStringPropertyFromMap(card, "faceName"); if (faceName.isEmpty()) { faceName = name; } // card properties - properties.clear(); - QMapIterator it(cardProperties); - while (it.hasNext()) { - it.next(); - QString mtgjsonProperty = it.key(); - QString xmlPropertyName = it.value(); + QVariantHash properties; + for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) properties.insert(xmlPropertyName, propertyValue); } // per-set properties - printingInfo = PrintingInfo(currentSet); - QMapIterator it2(setInfoProperties); - while (it2.hasNext()) { - it2.next(); - QString mtgjsonProperty = it2.key(); - QString xmlPropertyName = it2.value(); + PrintingInfo printingInfo = PrintingInfo(currentSet); + for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); if (!propertyValue.isEmpty()) printingInfo.setProperty(xmlPropertyName, propertyValue); } + // handle flavorNames specially due to double-faced cards + QString faceFlavorName = getStringPropertyFromMap(card, "faceFlavorName"); + QString flavorName = !faceFlavorName.isEmpty() ? faceFlavorName : getStringPropertyFromMap(card, "flavorName"); + if (!flavorName.isEmpty()) { + printingInfo.setProperty("flavorName", flavorName); + } + // Identifiers - QMapIterator it3(identifierProperties); - while (it3.hasNext()) { - it3.next(); - auto mtgjsonProperty = it3.key(); - auto xmlPropertyName = it3.value(); - auto propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); + for (auto i = identifierProperties.cbegin(), end = identifierProperties.cend(); i != end; ++i) { + QString mtgjsonProperty = i.key(); + QString xmlPropertyName = i.value(); + QString propertyValue = getStringPropertyFromMap(card.value("identifiers").toMap(), mtgjsonProperty); if (!propertyValue.isEmpty()) { printingInfo.setProperty(xmlPropertyName, propertyValue); } @@ -317,13 +326,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList allNameProps.append(faceName); // special handling properties - colors = card.value("colors").toStringList().join(""); + QString colors = card.value("colors").toStringList().join(""); if (!colors.isEmpty()) { properties.insert("colors", colors); } // special handling properties - colorIdentity = card.value("colorIdentity").toStringList().join(""); + QString colorIdentity = card.value("colorIdentity").toStringList().join(""); if (!colorIdentity.isEmpty()) { properties.insert("coloridentity", colorIdentity); } @@ -346,25 +355,23 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } auto legalities = card.value("legalities").toMap(); - for (const QString &fmtName : legalities.keys()) { - properties.insert(QString("format-%1").arg(fmtName), legalities.value(fmtName).toString().toLower()); + for (auto i = legalities.cbegin(), end = legalities.cend(); i != end; ++i) { + properties.insert(QString("format-%1").arg(i.key()), i.value().toString().toLower()); } // split cards are considered a single card, enqueue for later merging - if (layout == "split" || layout == "aftermath" || layout == "adventure") { + if (layout == "split" || layout == "aftermath" || layout == "adventure" || layout == "prepare") { auto _faceName = getStringPropertyFromMap(card, "faceName"); SplitCardPart split(_faceName, text, properties, printingInfo); auto found_iter = splitCards.find(name + numProperty); if (found_iter == splitCards.end()) { splitCards.insert(name + numProperty, {{split}, name}); - } else if (layout == "adventure") { - found_iter->first.insert(0, split); } else { found_iter->first.append(split); } } else { // relations - relatedCards.clear(); + QList relatedCards; // add other face for split cards as card relation if (!getStringPropertyFromMap(card, "side").isEmpty()) { @@ -379,12 +386,12 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList static const QRegularExpression meldNameRegex{"then meld them into ([^\\.]*)"}; QString additionalName = meldNameRegex.match(text).captured(1); if (!additionalName.isNull()) { - relatedCards.append(new CardRelation(additionalName, CardRelation::TransformInto)); + relatedCards.append(new CardRelation(additionalName, CardRelationType::TransformInto)); } } else { for (const QString &additionalName : name.split(" // ")) { if (additionalName != faceName) { - relatedCards.append(new CardRelation(additionalName, CardRelation::TransformInto)); + relatedCards.append(new CardRelation(additionalName, CardRelationType::TransformInto)); } } } @@ -399,7 +406,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList auto spbk = givenRelated.value("spellbook").toStringList(); for (const QString &spbkName : spbk) { relatedCards.append( - new CardRelation(spbkName, CardRelation::DoesNotAttach, false, false, 1, true)); + new CardRelation(spbkName, CardRelationType::DoesNotAttach, false, false, 1, true)); } } } @@ -412,17 +419,15 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList // split cards handling 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.value(nameSplit).first; - name = splitCards.value(nameSplit).second; + static const QList noRelatedCards = {}; - text.clear(); - properties.clear(); - relatedCards.clear(); + QList, QString>> partsAndNames = splitCards.values(); + for (auto [splitCardParts, name] : partsAndNames) { + QString text; + QVariantHash properties; + PrintingInfo printingInfo; for (const SplitCardPart &tmp : splitCardParts) { - QString splitName = tmp.getName(); if (!text.isEmpty()) { text.append(splitCardTextSeparator); } @@ -433,9 +438,10 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList printingInfo = tmp.getPrintingInfo(); } else { const QVariantHash &tmpProps = tmp.getProperties(); - for (const QString &prop : tmpProps.keys()) { + for (auto i = tmpProps.cbegin(), end = tmpProps.cend(); i != end; ++i) { + QString prop = i.key(); QString originalPropertyValue = properties.value(prop).toString(); - QString thisCardPropertyValue = tmpProps.value(prop).toString(); + QString thisCardPropertyValue = i.value().toString(); if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) { if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty properties.insert(prop, thisCardPropertyValue); @@ -451,24 +457,93 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList } } } - CardInfoPtr newCard = addCard(name, text, isToken, properties, relatedCards, printingInfo); + CardInfoPtr newCard = addCard(name, text, isToken, properties, noRelatedCards, printingInfo); numCards++; } return numCards; } +FormatRulesNameMap OracleImporter::createDefaultMagicFormats() +{ + // Predefined common exceptions + CardCondition superTypeIsBasic; + superTypeIsBasic.field = "type"; + superTypeIsBasic.matchType = "contains"; + superTypeIsBasic.value = "Basic Land"; + + ExceptionRule basicLands; + basicLands.conditions.append(superTypeIsBasic); + + CardCondition anyNumberAllowed; + anyNumberAllowed.field = "text"; + anyNumberAllowed.matchType = "contains"; + anyNumberAllowed.value = "A deck can have any number of"; + + ExceptionRule mayContainAnyNumber; + mayContainAnyNumber.conditions.append(anyNumberAllowed); + + // Map to store default rules + FormatRulesNameMap defaultFormatRulesNameMap; + + // ----------------- Helper lambda to create format ----------------- + auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, + const QList &allowedCounts = kConstructedCounts) -> FormatRulesPtr { + FormatRulesPtr f(new FormatRules); + f->formatName = name; + f->allowedCounts = allowedCounts; + f->minDeckSize = minDeck; + f->maxDeckSize = maxDeck; + f->maxSideboardSize = maxSideboardSize; + f->exceptions.append(basicLands); + f->exceptions.append(mayContainAnyNumber); + defaultFormatRulesNameMap.insert(name.toLower(), f); + return f; + }; + + // ----------------- Standard formats ----------------- + makeFormat("Standard"); + makeFormat("Modern"); + makeFormat("Legacy"); + makeFormat("Pioneer"); + makeFormat("Historic"); + makeFormat("Timeless"); + makeFormat("Future"); + makeFormat("OldSchool"); + makeFormat("Premodern"); + makeFormat("Pauper"); + makeFormat("Penny"); + + // ----------------- Singleton formats ----------------- + makeFormat("Commander", 100, 100, 15, kSingletonCounts); + makeFormat("Duel", 100, 100, 15, kSingletonCounts); + makeFormat("Brawl", 60, 60, 15, kSingletonCounts); + makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts); + makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts); + makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts); + makeFormat("Predh", 100, 100, 15, kSingletonCounts); + + // ----------------- Restricted formats ----------------- + makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}}); + + return defaultFormatRulesNameMap; +} + int OracleImporter::startImport() { - int setCards = 0, setIndex = 0; + static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); + // add an empty set for tokens - CardSetPtr tokenSet = CardSet::newInstance(CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); + CardSetPtr tokenSet = + CardSet::newInstance(noOpController, CardSet::TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); sets.insert(CardSet::TOKENS_SETNAME, tokenSet); + int setIndex = 0; + for (const SetToDownload &curSetToParse : allSets) { - CardSetPtr newSet = - CardSet::newInstance(curSetToParse.getShortName(), curSetToParse.getLongName(), curSetToParse.getSetType(), - curSetToParse.getReleaseDate(), curSetToParse.getPriority()); + CardSetPtr newSet = CardSet::newInstance(noOpController, curSetToParse.getShortName(), + curSetToParse.getLongName(), curSetToParse.getSetType(), + curSetToParse.getReleaseDate(), curSetToParse.getPriority()); if (!sets.contains(newSet->getShortName())) sets.insert(newSet->getShortName(), newSet); @@ -479,7 +554,7 @@ int OracleImporter::startImport() emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } - emit setIndexChanged(setCards, setIndex, QString()); + emit setIndexChanged(0, setIndex, QString()); // total number of sets return setIndex; @@ -487,8 +562,9 @@ int OracleImporter::startImport() bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) { - CockatriceXml4Parser parser; - return parser.saveToFile(sets, cards, fileName, sourceUrl, sourceVersion); + CockatriceXml4Parser parser(new NoopCardPreferenceProvider(), new NoopCardSetPriorityController()); + + return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); } void OracleImporter::clear() diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 4d5d77805..5bf352594 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include // many users prefer not to see these sets with non english arts @@ -22,6 +22,7 @@ const QMap setTypePriorities{ {"archenemy", CardSet::PriorityReprint}, {"arsenal", CardSet::PriorityReprint}, {"box", CardSet::PriorityReprint}, + {"eternal", CardSet::PriorityReprint}, {"from_the_vault", CardSet::PriorityReprint}, {"masterpiece", CardSet::PriorityReprint}, {"masters", CardSet::PriorityReprint}, @@ -154,6 +155,7 @@ public: int startImport(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList); + FormatRulesNameMap createDefaultMagicFormats(); const CardNameMap &getCardList() const { return cards; diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 0fdd4b8f1..2edb9e561 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -1,63 +1,38 @@ #include "oraclewizard.h" +#include "client/settings/cache_settings.h" #include "main.h" #include "oracleimporter.h" -#include "settings/cache_settings.h" -#include "version_string.h" +#include "pages.h" +#include "pagetemplates.h" -#include -#include #include -#include -#include #include #include -#include #include -#include -#include #include -#include #include -#include #include -#include -#include #include #include -#ifdef HAS_LZMA -#include "lzma/decompress.h" -#endif - -#ifdef HAS_ZLIB -#include "zip/unzip.h" -#endif - -#define ZIP_SIGNATURE "PK" -// Xz stream header: 0xFD + "7zXZ" -#define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A" -#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/api/v5/AllPrintings.json.xz" -#elif defined(HAS_ZLIB) -#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.zip" -#else -#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" -#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" - OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) { // define a dummy context that will be used where needed QString dummy = QT_TRANSLATE_NOOP("i18n", "English"); - settings = new QSettings(SettingsCache::instance().getSettingsPath() + "global.ini", QSettings::IniFormat, this); +#ifdef Q_OS_WIN + setWizardStyle(QWizard::ModernStyle); +#endif + + QString oracleSettingsFile = SettingsCache::instance().getSettingsPath() + "oracle.ini"; + settings = new QSettings(oracleSettingsFile, QSettings::IniFormat, this); + + // We moved the oracle-specific settings from global.ini to a separate oracle.ini after 2.10 + if (!QFile::exists(oracleSettingsFile)) { + migrateOracleSettings(); + } + connect(&SettingsCache::instance(), &SettingsCache::langChanged, this, &OracleWizard::updateLanguage); importer = new OracleImporter(this); @@ -86,6 +61,26 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent) retranslateUi(); } +/** + * Migrates the oracle-specific settings from global.ini to oracle.ini + */ +void OracleWizard::migrateOracleSettings() +{ + QString filePath = SettingsCache::instance().getSettingsPath() + "global.ini"; + auto globalSettings = QSettings(filePath, QSettings::IniFormat, this); + + auto tryMigrateValue = [this, &globalSettings](const QString &name) { + QVariant variant = globalSettings.value(name); + if (variant.isValid()) { + settings->setValue(name, variant.toString()); + } + }; + + tryMigrateValue("allsetsurl"); + tryMigrateValue("tokensurl"); + tryMigrateValue("spoilersurl"); +} + void OracleWizard::updateLanguage() { qApp->removeTranslator(translator); @@ -142,667 +137,3 @@ bool OracleWizard::saveTokensToFile(const QString &fileName) file.close(); return true; } - -IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent) -{ - label = new QLabel(this); - label->setWordWrap(true); - - languageLabel = new QLabel(this); - versionLabel = new QLabel(this); - languageBox = new QComboBox(this); - - QStringList languageCodes = findQmFiles(); - for (const QString &code : languageCodes) { - QString langName = languageName(code); - languageBox->addItem(langName, code); - } - - QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME); - int index = languageBox->findText(setLanguage, Qt::MatchExactly); - if (index == -1) { - qWarning() << "could not find language" << setLanguage; - } else { - languageBox->setCurrentIndex(index); - } - - connect(languageBox, qOverload(&QComboBox::currentIndexChanged), this, &IntroPage::languageBoxChanged); - - auto *layout = new QGridLayout(this); - layout->addWidget(label, 0, 0, 1, 2); - layout->addWidget(languageLabel, 1, 0); - layout->addWidget(languageBox, 1, 1); - layout->addWidget(versionLabel, 2, 0, 1, 2); - - setLayout(layout); -} - -void IntroPage::initializePage() -{ - if (wizard()->backgroundMode) { - emit readyToContinue(); - } -} - -QStringList IntroPage::findQmFiles() -{ - QDir dir(translationPath); - QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name); - fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1"); - return fileNames; -} - -QString IntroPage::languageName(const QString &lang) -{ - QTranslator qTranslator; - - QString appNameHint = translationPrefix + "_" + lang; - bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath); - if (!appTranslationLoaded) { - qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; - } - - return qTranslator.translate("i18n", DEFAULT_LANG_NAME); -} - -void IntroPage::languageBoxChanged(int index) -{ - SettingsCache::instance().setLang(languageBox->itemData(index).toString()); -} - -void IntroPage::retranslateUi() -{ - setTitle(tr("Introduction")); - label->setText(tr("This wizard will import the list of sets, cards, and tokens " - "that will be used by Cockatrice.")); - languageLabel->setText(tr("Interface language:")); - versionLabel->setText(tr("Version:") + QString(" %1").arg(VERSION_STRING)); -} - -void OutroPage::retranslateUi() -{ - setTitle(tr("Finished")); - setSubTitle(tr("The wizard has finished.") + "
" + - tr("You can now start using Cockatrice with the newly updated cards.") + "

" + - tr("If the card databases don't reload automatically, restart the Cockatrice client.")); -} - -void OutroPage::initializePage() -{ - if (wizard()->backgroundMode) { - wizard()->accept(); - exit(0); - } -} - -LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent) -{ - urlRadioButton = new QRadioButton(this); - fileRadioButton = new QRadioButton(this); - - urlLineEdit = new QLineEdit(this); - fileLineEdit = new QLineEdit(this); - - progressLabel = new QLabel(this); - progressBar = new QProgressBar(this); - - urlRadioButton->setChecked(true); - - urlButton = new QPushButton(this); - connect(urlButton, &QPushButton::clicked, this, &LoadSetsPage::actRestoreDefaultUrl); - - fileButton = new QPushButton(this); - connect(fileButton, &QPushButton::clicked, this, &LoadSetsPage::actLoadSetsFile); - - auto *layout = new QGridLayout(this); - layout->addWidget(urlRadioButton, 0, 0); - layout->addWidget(urlLineEdit, 0, 1); - layout->addWidget(urlButton, 1, 1, Qt::AlignRight); - layout->addWidget(fileRadioButton, 2, 0); - layout->addWidget(fileLineEdit, 2, 1); - layout->addWidget(fileButton, 3, 1, Qt::AlignRight); - layout->addWidget(progressLabel, 4, 0); - layout->addWidget(progressBar, 4, 1); - - connect(&watcher, &QFutureWatcher::finished, this, &LoadSetsPage::importFinished); - - setLayout(layout); -} - -void LoadSetsPage::initializePage() -{ - urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); - - progressLabel->hide(); - progressBar->hide(); - - if (wizard()->backgroundMode) { - if (isEnabled()) { - validatePage(); - } - } -} - -void LoadSetsPage::retranslateUi() -{ - setTitle(tr("Source selection")); - setSubTitle(tr("Please specify a compatible source for the list of sets and cards. " - "You can specify a URL address that will be downloaded or " - "use an existing file from your computer.")); - - urlRadioButton->setText(tr("Download URL:")); - fileRadioButton->setText(tr("Local file:")); - urlButton->setText(tr("Restore default URL")); - fileButton->setText(tr("Choose file...")); -} - -void LoadSetsPage::actRestoreDefaultUrl() -{ - urlLineEdit->setText(ALLSETS_URL); -} - -void LoadSetsPage::actLoadSetsFile() -{ - QFileDialog dialog(this, tr("Load sets file")); - dialog.setFileMode(QFileDialog::ExistingFile); - - QString extensions = "*.json *.xml"; -#ifdef HAS_ZLIB - extensions += " *.zip"; -#endif -#ifdef HAS_LZMA - extensions += " *.xz"; -#endif - dialog.setNameFilter(tr("Sets file (%1)").arg(extensions)); - - if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { - dialog.selectFile(fileLineEdit->text()); - } - - if (!dialog.exec()) { - return; - } - - fileLineEdit->setText(dialog.selectedFiles().at(0)); -} - -bool LoadSetsPage::validatePage() -{ - // once the import is finished, we call next(); skip validation - if (wizard()->downloadedPlainXml || wizard()->importer->getSets().count() > 0) { - return true; - } - - // else, try to import sets - if (urlRadioButton->isChecked()) { - // 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; - } - - progressLabel->setText(tr("Downloading (0MB)")); - // show an infinite progressbar - progressBar->setMaximum(0); - progressBar->setMinimum(0); - progressBar->setValue(0); - progressLabel->show(); - progressBar->show(); - - wizard()->disableButtons(); - setEnabled(false); - - downloadSetsFile(url); - } else if (fileRadioButton->isChecked()) { - QFile setsFile(fileLineEdit->text()); - if (!setsFile.exists()) { - QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); - return false; - } - - if (!setsFile.open(QIODevice::ReadOnly)) { - QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); - return false; - } - - wizard()->disableButtons(); - setEnabled(false); - - wizard()->setCardSourceUrl(setsFile.fileName()); - wizard()->setCardSourceVersion("unknown"); - - readSetsFromByteArray(setsFile.readAll()); - } - - return false; -} - -void LoadSetsPage::downloadSetsFile(const QUrl &url) -{ - wizard()->setCardSourceVersion("unknown"); - - const auto urlString = url.toString(); - if (urlString == ALLSETS_URL || urlString == ALLSETS_URL_FALLBACK) { - 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) { - auto jsonData = versionReply->readAll(); - QJsonParseError jsonError{}; - auto jsonResponse = QJsonDocument::fromJson(jsonData, &jsonError); - - if (jsonError.error == QJsonParseError::NoError) { - const auto jsonMap = jsonResponse.toVariant().toMap(); - - auto versionString = jsonMap.value("meta").toMap().value("version").toString(); - if (versionString.isEmpty()) { - versionString = "unknown"; - } - wizard()->setCardSourceVersion(versionString); - } - } - - versionReply->deleteLater(); - }); - } - - wizard()->setCardSourceUrl(url.toString()); - - auto *reply = wizard()->nam->get(QNetworkRequest(url)); - - connect(reply, &QNetworkReply::finished, this, &LoadSetsPage::actDownloadFinishedSetsFile); - connect(reply, &QNetworkReply::downloadProgress, this, &LoadSetsPage::actDownloadProgressSetsFile); -} - -void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) -{ - if (total > 0) { - progressBar->setMaximum(static_cast(total)); - progressBar->setValue(static_cast(received)); - } - progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); -} - -void LoadSetsPage::actDownloadFinishedSetsFile() -{ - // check for a reply - auto *reply = dynamic_cast(sender()); - auto errorCode = reply->error(); - if (errorCode != QNetworkReply::NoError) { - QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); - - wizard()->enableButtons(); - setEnabled(true); - - reply->deleteLater(); - return; - } - - auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 301 || statusCode == 302) { - const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - qDebug() << "following redirect url:" << redirectUrl.toString(); - downloadSetsFile(redirectUrl); - reply->deleteLater(); - return; - } - - progressLabel->hide(); - progressBar->hide(); - - // 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 { - wizard()->settings->remove("allsetsurl"); - } - - readSetsFromByteArray(reply->readAll()); - reply->deleteLater(); -} - -void LoadSetsPage::readSetsFromByteArray(QByteArray _data) -{ - // show an infinite progressbar - progressBar->setMaximum(0); - progressBar->setMinimum(0); - progressBar->setValue(0); - progressLabel->setText(tr("Parsing file")); - progressLabel->show(); - progressBar->show(); - - wizard()->downloadedPlainXml = false; - wizard()->xmlData.clear(); - readSetsFromByteArrayRef(_data); -} - -void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data) -{ - // unzip the file if needed - if (_data.startsWith(XZ_SIGNATURE)) { -#ifdef HAS_LZMA - // zipped file - auto *inBuffer = new QBuffer(&_data); - auto newData = QByteArray(); - auto *outBuffer = new QBuffer(&newData); - inBuffer->open(QBuffer::ReadOnly); - outBuffer->open(QBuffer::WriteOnly); - XzDecompressor xz; - if (!xz.decompress(inBuffer, outBuffer)) { - zipDownloadFailed(tr("Xz extraction failed.")); - return; - } - _data.clear(); - readSetsFromByteArrayRef(newData); - return; -#else - zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); - - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - return; -#endif - } else if (_data.startsWith(ZIP_SIGNATURE)) { -#ifdef HAS_ZLIB - // zipped file - auto *inBuffer = new QBuffer(&_data); - auto newData = QByteArray(); - auto *outBuffer = new QBuffer(&newData); - QString fileName; - UnZip::ErrorCode ec; - UnZip uz; - - ec = uz.openArchive(inBuffer); - if (ec != UnZip::Ok) { - zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec))); - return; - } - - if (uz.fileList().size() != 1) { - zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.")); - return; - } - fileName = uz.fileList().at(0); - - outBuffer->open(QBuffer::ReadWrite); - ec = uz.extractFile(fileName, outBuffer); - if (ec != UnZip::Ok) { - zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec))); - uz.closeArchive(); - return; - } - _data.clear(); - readSetsFromByteArrayRef(newData); - return; -#else - zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files.")); - - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - return; -#endif - } else if (_data.startsWith("{")) { - // Start the computation. - jsonData = std::move(_data); - future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); }); - watcher.setFuture(future); - } else if (_data.startsWith("<")) { - // save xml file and don't do any processing - wizard()->downloadedPlainXml = true; - wizard()->xmlData = std::move(_data); - importFinished(); - } else { - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data.")); - } -} - -void LoadSetsPage::zipDownloadFailed(const QString &message) -{ - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - - QMessageBox::StandardButton reply; - reply = static_cast(QMessageBox::question( - this, tr("Error"), message + "
" + tr("Do you want to download the uncompressed file instead?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)); - - if (reply == QMessageBox::Yes) { - urlRadioButton->setChecked(true); - urlLineEdit->setText(ALLSETS_URL_FALLBACK); - - wizard()->next(); - } -} - -void LoadSetsPage::importFinished() -{ - wizard()->enableButtons(); - setEnabled(true); - progressLabel->hide(); - progressBar->hide(); - - if (wizard()->downloadedPlainXml || watcher.future().result()) { - wizard()->next(); - } else { - QMessageBox::critical(this, tr("Error"), - tr("The file was retrieved successfully, but it does not contain any sets data.")); - } -} - -SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) -{ - pathLabel = new QLabel(this); - saveLabel = new QLabel(this); - - defaultPathCheckBox = new QCheckBox(this); - - messageLog = new QTextEdit(this); - messageLog->setReadOnly(true); - - auto *layout = new QGridLayout(this); - layout->addWidget(messageLog, 0, 0); - layout->addWidget(saveLabel, 1, 0); - layout->addWidget(pathLabel, 2, 0); - layout->addWidget(defaultPathCheckBox, 3, 0); - - setLayout(layout); -} - -void SaveSetsPage::cleanupPage() -{ - wizard()->importer->clear(); - disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr); -} - -void SaveSetsPage::initializePage() -{ - messageLog->clear(); - - retranslateUi(); - if (wizard()->downloadedPlainXml) { - messageLog->hide(); - return; - } - messageLog->show(); - connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); - - if (!wizard()->importer->startImport()) { - QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); - } - - if (wizard()->backgroundMode) { - emit readyToContinue(); - } -} - -void SaveSetsPage::retranslateUi() -{ - setTitle(tr("Sets imported")); - if (wizard()->downloadedPlainXml) { - setSubTitle(tr("A cockatrice database file of %1 MB has been downloaded.") - .arg(qRound(wizard()->xmlData.size() / 1000000.0))); - } else { - setSubTitle(tr("The following sets have been found:")); - } - - saveLabel->setText(tr("Press \"Save\" to store the imported cards in the Cockatrice database.")); - pathLabel->setText(tr("The card database will be saved at the following location:") + "
" + - SettingsCache::instance().getCardDatabasePath()); - defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); - - setButtonText(QWizard::NextButton, tr("&Save")); -} - -void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName) -{ - if (setName.isEmpty()) { - messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + - ""); - } else { - messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); - } - - messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); -} - -bool SaveSetsPage::validatePage() -{ - QString defaultPath = SettingsCache::instance().getCardDatabasePath(); - QString windowName = tr("Save card database"); - QString fileType = tr("XML; card database (*.xml)"); - - QString fileName; - if (defaultPathCheckBox->isChecked()) { - fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); - } else { - fileName = defaultPath; - } - - if (fileName.isEmpty()) { - return false; - } - - QFileInfo fi(fileName); - QDir fileDir(fi.path()); - if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { - return false; - } - - if (wizard()->downloadedPlainXml) { - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { - qDebug() << "File write (w) failed for" << fileName; - return false; - } - if (file.write(wizard()->xmlData) < 1) { - qDebug() << "File write (w) failed for" << fileName; - return false; - } - wizard()->xmlData.clear(); - } else 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; -} - -void LoadTokensPage::initializePage() -{ - SimpleDownloadFilePage::initializePage(); - - if (wizard()->backgroundMode) { - emit readyToContinue(); - } -} - -QString LoadTokensPage::getDefaultUrl() -{ - return TOKENS_URL; -} - -QString LoadTokensPage::getCustomUrlSettingsKey() -{ - return "tokensurl"; -} - -QString LoadTokensPage::getDefaultSavePath() -{ - return SettingsCache::instance().getTokenDatabasePath(); -} - -QString LoadTokensPage::getWindowTitle() -{ - return tr("Save token database"); -} - -QString LoadTokensPage::getFileType() -{ - return tr("XML; token database (*.xml)"); -} - -void LoadTokensPage::retranslateUi() -{ - setTitle(tr("Tokens import")); - setSubTitle(tr("Please specify a compatible source for token data.")); - - urlLabel->setText(tr("Download URL:")); - urlButton->setText(tr("Restore default URL")); - pathLabel->setText(tr("The token database will be saved at the following location:") + "
" + - SettingsCache::instance().getTokenDatabasePath()); - defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); -} - -QString LoadSpoilersPage::getDefaultUrl() -{ - return SPOILERS_URL; -} - -QString LoadSpoilersPage::getCustomUrlSettingsKey() -{ - return "spoilersurl"; -} - -QString LoadSpoilersPage::getDefaultSavePath() -{ - return SettingsCache::instance().getTokenDatabasePath(); -} - -QString LoadSpoilersPage::getWindowTitle() -{ - return tr("Save spoiler database"); -} - -QString LoadSpoilersPage::getFileType() -{ - return tr("XML; spoiler database (*.xml)"); -} - -void LoadSpoilersPage::retranslateUi() -{ - setTitle(tr("Spoilers import")); - setSubTitle(tr("Please specify a compatible source for spoiler data.")); - - urlLabel->setText(tr("Download URL:")); - urlButton->setText(tr("Restore default URL")); - pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "
" + - SettingsCache::instance().getSpoilerCardDatabasePath()); - defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); -} diff --git a/oracle/src/oraclewizard.h b/oracle/src/oraclewizard.h index 2733bf1ad..78427175c 100644 --- a/oracle/src/oraclewizard.h +++ b/oracle/src/oraclewizard.h @@ -1,9 +1,6 @@ #ifndef ORACLEWIZARD_H #define ORACLEWIZARD_H -#include -#include -#include #include #include @@ -20,8 +17,6 @@ class QVBoxLayout; class OracleImporter; class QSettings; -#include "pagetemplates.h" - class OracleWizard : public QWizard { Q_OBJECT @@ -80,133 +75,10 @@ private: QString cardSourceUrl; QString cardSourceVersion; + void migrateOracleSettings(); + protected: void changeEvent(QEvent *event) override; }; -class IntroPage : public OracleWizardPage -{ - Q_OBJECT -public: - explicit IntroPage(QWidget *parent = nullptr); - void retranslateUi() override; - -private: - QStringList findQmFiles(); - QString languageName(const QString &lang); - -private: - QLabel *label, *languageLabel, *versionLabel; - QComboBox *languageBox; - -private slots: - void languageBoxChanged(int index); - -protected slots: - void initializePage() override; -}; - -class OutroPage : public OracleWizardPage -{ - Q_OBJECT -public: - explicit OutroPage(QWidget * = nullptr) - { - } - void retranslateUi() override; - -protected: - void initializePage() override; -}; - -class LoadSetsPage : public OracleWizardPage -{ - Q_OBJECT -public: - explicit LoadSetsPage(QWidget *parent = nullptr); - void retranslateUi() override; - -protected: - void initializePage() override; - bool validatePage() override; - void readSetsFromByteArray(QByteArray _data); - void readSetsFromByteArrayRef(QByteArray &_data); - void downloadSetsFile(const QUrl &url); - -private: - QRadioButton *urlRadioButton; - QRadioButton *fileRadioButton; - QLineEdit *urlLineEdit; - QLineEdit *fileLineEdit; - QPushButton *urlButton; - QPushButton *fileButton; - QLabel *progressLabel; - QProgressBar *progressBar; - - QFutureWatcher watcher; - QFuture future; - QByteArray jsonData; - -private slots: - void actLoadSetsFile(); - void actRestoreDefaultUrl(); - void actDownloadProgressSetsFile(qint64 received, qint64 total); - void actDownloadFinishedSetsFile(); - void importFinished(); - void zipDownloadFailed(const QString &message); -}; - -class SaveSetsPage : public OracleWizardPage -{ - Q_OBJECT -public: - explicit SaveSetsPage(QWidget *parent = nullptr); - void retranslateUi() override; - -private: - QTextEdit *messageLog; - QCheckBox *defaultPathCheckBox; - QLabel *pathLabel; - QLabel *saveLabel; - -protected: - void initializePage() override; - void cleanupPage() override; - bool validatePage() override; - -private slots: - void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); -}; - -class LoadSpoilersPage : public SimpleDownloadFilePage -{ - Q_OBJECT -public: - explicit LoadSpoilersPage(QWidget * = nullptr){}; - void retranslateUi() override; - -protected: - QString getDefaultUrl() override; - QString getCustomUrlSettingsKey() override; - QString getDefaultSavePath() override; - QString getWindowTitle() override; - QString getFileType() override; -}; - -class LoadTokensPage : public SimpleDownloadFilePage -{ - Q_OBJECT -public: - explicit LoadTokensPage(QWidget * = nullptr){}; - void retranslateUi() override; - -protected: - QString getDefaultUrl() override; - QString getCustomUrlSettingsKey() override; - QString getDefaultSavePath() override; - QString getWindowTitle() override; - QString getFileType() override; - void initializePage() override; -}; - #endif diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp new file mode 100644 index 000000000..7629a291b --- /dev/null +++ b/oracle/src/pages.cpp @@ -0,0 +1,742 @@ +#include "pages.h" + +#include "client/settings/cache_settings.h" +#include "main.h" +#include "oracleimporter.h" +#include "oraclewizard.h" +#include "pages.h" +#include "pagetemplates.h" +#include "version_string.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAS_LZMA +#include "lzma/decompress.h" +#endif + +#ifdef HAS_ZLIB +#include "zip/unzip.h" +#endif + +#define ZIP_SIGNATURE "PK" +// Xz stream header: 0xFD + "7zXZ" +#define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A" +#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/api/v5/AllPrintings.json.xz" +#elif defined(HAS_ZLIB) +#define ALLSETS_URL "https://www.mtgjson.com/api/v5/AllPrintings.json.zip" +#else +#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" +#define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml" + +IntroPage::IntroPage(QWidget *parent) : OracleWizardPage(parent) +{ + label = new QLabel(this); + label->setWordWrap(true); + + languageLabel = new QLabel(this); + versionLabel = new QLabel(this); + languageBox = new QComboBox(this); + + QStringList languageCodes = findQmFiles(); + for (const QString &code : languageCodes) { + QString langName = languageName(code); + languageBox->addItem(langName, code); + } + + QString setLanguage = QCoreApplication::translate("i18n", DEFAULT_LANG_NAME); + int index = languageBox->findText(setLanguage, Qt::MatchExactly); + if (index == -1) { + qWarning() << "could not find language" << setLanguage; + } else { + languageBox->setCurrentIndex(index); + } + + connect(languageBox, qOverload(&QComboBox::currentIndexChanged), this, &IntroPage::languageBoxChanged); + + auto *layout = new QGridLayout(this); + layout->addWidget(label, 0, 0, 1, 2); + layout->addWidget(languageLabel, 1, 0); + layout->addWidget(languageBox, 1, 1); + layout->addWidget(versionLabel, 2, 0, 1, 2); + + setLayout(layout); +} + +void IntroPage::initializePage() +{ + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +QStringList IntroPage::findQmFiles() +{ + QDir dir(translationPath); + QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name); + fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1"); + return fileNames; +} + +QString IntroPage::languageName(const QString &lang) +{ + QTranslator qTranslator; + + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } + + return qTranslator.translate("i18n", DEFAULT_LANG_NAME); +} + +void IntroPage::languageBoxChanged(int index) +{ + SettingsCache::instance().setLang(languageBox->itemData(index).toString()); +} + +void IntroPage::retranslateUi() +{ + setTitle(tr("Introduction")); + label->setText(tr("This wizard will import the list of sets, cards, and tokens " + "that will be used by Cockatrice.")); + languageLabel->setText(tr("Interface language:")); + versionLabel->setText(tr("Version:") + QString(" %1").arg(VERSION_STRING)); +} + +void OutroPage::retranslateUi() +{ + setTitle(tr("Finished")); + setSubTitle(tr("The wizard has finished.") + "
" + + tr("You can now start using Cockatrice with the newly updated cards.") + "

" + + tr("If the card databases don't reload automatically, restart the Cockatrice client.")); +} + +void OutroPage::initializePage() +{ + if (wizard()->backgroundMode) { + wizard()->accept(); + exit(0); + } +} + +LoadSetsPage::LoadSetsPage(QWidget *parent) : OracleWizardPage(parent) +{ + urlRadioButton = new QRadioButton(this); + fileRadioButton = new QRadioButton(this); + + urlLineEdit = new QLineEdit(this); + fileLineEdit = new QLineEdit(this); + + progressLabel = new QLabel(this); + progressBar = new QProgressBar(this); + + urlRadioButton->setChecked(true); + + urlButton = new QPushButton(this); + connect(urlButton, &QPushButton::clicked, this, &LoadSetsPage::actRestoreDefaultUrl); + + fileButton = new QPushButton(this); + connect(fileButton, &QPushButton::clicked, this, &LoadSetsPage::actLoadSetsFile); + + auto *layout = new QGridLayout(this); + layout->addWidget(urlRadioButton, 0, 0); + layout->addWidget(urlLineEdit, 0, 1); + layout->addWidget(urlButton, 1, 1, Qt::AlignRight); + layout->addWidget(fileRadioButton, 2, 0); + layout->addWidget(fileLineEdit, 2, 1); + layout->addWidget(fileButton, 3, 1, Qt::AlignRight); + layout->addWidget(progressLabel, 4, 0); + layout->addWidget(progressBar, 4, 1); + + connect(&watcher, &QFutureWatcher::finished, this, &LoadSetsPage::importFinished); + + setLayout(layout); +} + +void LoadSetsPage::initializePage() +{ + urlLineEdit->setText(wizard()->settings->value("allsetsurl", ALLSETS_URL).toString()); + + progressLabel->hide(); + progressBar->hide(); + + if (wizard()->backgroundMode) { + if (isEnabled()) { + validatePage(); + } + } +} + +void LoadSetsPage::retranslateUi() +{ + setTitle(tr("Source selection")); + setSubTitle(tr("Please specify a compatible source for the list of sets and cards. " + "You can specify a URL address that will be downloaded or " + "use an existing file from your computer.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); +} + +void LoadSetsPage::actRestoreDefaultUrl() +{ + urlLineEdit->setText(ALLSETS_URL); +} + +void LoadSetsPage::actLoadSetsFile() +{ + QFileDialog dialog(this, tr("Load sets file")); + dialog.setFileMode(QFileDialog::ExistingFile); + + QString extensions = "*.json *.xml"; +#ifdef HAS_ZLIB + extensions += " *.zip"; +#endif +#ifdef HAS_LZMA + extensions += " *.xz"; +#endif + dialog.setNameFilter(tr("Sets file (%1)").arg(extensions)); + + if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { + dialog.selectFile(fileLineEdit->text()); + } + + if (!dialog.exec()) { + return; + } + + fileLineEdit->setText(dialog.selectedFiles().at(0)); +} + +bool LoadSetsPage::validatePage() +{ + // once the import is finished, we call next(); skip validation + if (wizard()->downloadedPlainXml || wizard()->importer->getSets().count() > 0) { + return true; + } + + // else, try to import sets + if (urlRadioButton->isChecked()) { + // 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; + } + + progressLabel->setText(tr("Downloading (0MB)")); + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->show(); + progressBar->show(); + + wizard()->disableButtons(); + setEnabled(false); + + downloadSetsFile(url); + } else if (fileRadioButton->isChecked()) { + QFile setsFile(fileLineEdit->text()); + if (!setsFile.exists()) { + QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); + return false; + } + + if (!setsFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + + wizard()->disableButtons(); + setEnabled(false); + + wizard()->setCardSourceUrl(setsFile.fileName()); + wizard()->setCardSourceVersion("unknown"); + + readSetsFromByteArray(setsFile.readAll()); + } + + return false; +} + +void LoadSetsPage::downloadSetsFile(const QUrl &url) +{ + wizard()->setCardSourceVersion("unknown"); + + const auto urlString = url.toString(); + if (urlString == ALLSETS_URL || urlString == ALLSETS_URL_FALLBACK) { + const auto versionUrl = QUrl::fromUserInput(MTGJSON_VERSION_URL); + QNetworkRequest request = QNetworkRequest(versionUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + auto *versionReply = wizard()->nam->get(request); + connect(versionReply, &QNetworkReply::finished, [this, versionReply]() { + if (versionReply->error() == QNetworkReply::NoError) { + auto data = versionReply->readAll(); + QJsonParseError jsonError{}; + auto jsonResponse = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error == QJsonParseError::NoError) { + const auto jsonMap = jsonResponse.toVariant().toMap(); + + auto versionString = jsonMap.value("meta").toMap().value("version").toString(); + if (versionString.isEmpty()) { + versionString = "unknown"; + } + wizard()->setCardSourceVersion(versionString); + } + } + + versionReply->deleteLater(); + }); + } + + wizard()->setCardSourceUrl(url.toString()); + + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + auto *reply = wizard()->nam->get(request); + + connect(reply, &QNetworkReply::finished, this, &LoadSetsPage::actDownloadFinishedSetsFile); + connect(reply, &QNetworkReply::downloadProgress, this, &LoadSetsPage::actDownloadProgressSetsFile); +} + +void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total) +{ + if (total > 0) { + progressBar->setMaximum(static_cast(total)); + progressBar->setValue(static_cast(received)); + } + progressLabel->setText(tr("Downloading (%1MB)").arg((int)received / (1024 * 1024))); +} + +void LoadSetsPage::actDownloadFinishedSetsFile() +{ + // check for a reply + auto *reply = dynamic_cast(sender()); + auto errorCode = reply->error(); + if (errorCode != QNetworkReply::NoError) { + QMessageBox::critical(this, tr("Error"), tr("Network error: %1.").arg(reply->errorString())); + + wizard()->enableButtons(); + setEnabled(true); + + reply->deleteLater(); + return; + } + + auto statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (statusCode == 301 || statusCode == 302) { + const auto redirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + qDebug() << "following redirect url:" << redirectUrl.toString(); + downloadSetsFile(redirectUrl); + reply->deleteLater(); + return; + } + + progressLabel->hide(); + progressBar->hide(); + + // 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 { + wizard()->settings->remove("allsetsurl"); + } + + readSetsFromByteArray(reply->readAll()); + reply->deleteLater(); +} + +void LoadSetsPage::readSetsFromByteArray(QByteArray _data) +{ + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->setText(tr("Parsing file")); + progressLabel->show(); + progressBar->show(); + + wizard()->downloadedPlainXml = false; + wizard()->xmlData.clear(); + readSetsFromByteArrayRef(_data); +} + +void LoadSetsPage::readSetsFromByteArrayRef(QByteArray &_data) +{ + // unzip the file if needed + if (_data.startsWith(XZ_SIGNATURE)) { +#ifdef HAS_LZMA + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + inBuffer->open(QBuffer::ReadOnly); + outBuffer->open(QBuffer::WriteOnly); + XzDecompressor xz; + if (!xz.decompress(inBuffer, outBuffer)) { + zipDownloadFailed(tr("Xz extraction failed.")); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith(ZIP_SIGNATURE)) { +#ifdef HAS_ZLIB + // zipped file + auto *inBuffer = new QBuffer(&_data); + auto newData = QByteArray(); + auto *outBuffer = new QBuffer(&newData); + QString fileName; + UnZip::ErrorCode ec; + UnZip uz; + + ec = uz.openArchive(inBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Failed to open Zip archive: %1.").arg(uz.formatError(ec))); + return; + } + + if (uz.fileList().size() != 1) { + zipDownloadFailed(tr("Zip extraction failed: the Zip archive doesn't contain exactly one file.")); + return; + } + fileName = uz.fileList().at(0); + + outBuffer->open(QBuffer::ReadWrite); + ec = uz.extractFile(fileName, outBuffer); + if (ec != UnZip::Ok) { + zipDownloadFailed(tr("Zip extraction failed: %1.").arg(uz.formatError(ec))); + uz.closeArchive(); + return; + } + _data.clear(); + readSetsFromByteArrayRef(newData); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support zipped files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (_data.startsWith("{")) { + // Start the computation. + jsonData = std::move(_data); + future = QtConcurrent::run([this] { return wizard()->importer->readSetsFromByteArray(std::move(jsonData)); }); + watcher.setFuture(future); + } else if (_data.startsWith("<")) { + // save xml file and don't do any processing + wizard()->downloadedPlainXml = true; + wizard()->xmlData = std::move(_data); + importFinished(); + } else { + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + QMessageBox::critical(this, tr("Error"), tr("Failed to interpret downloaded data.")); + } +} + +void LoadSetsPage::zipDownloadFailed(const QString &message) +{ + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + + QMessageBox::StandardButton reply; + reply = static_cast(QMessageBox::question( + this, tr("Error"), message + "
" + tr("Do you want to download the uncompressed file instead?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)); + + if (reply == QMessageBox::Yes) { + urlRadioButton->setChecked(true); + urlLineEdit->setText(ALLSETS_URL_FALLBACK); + + wizard()->next(); + } +} + +void LoadSetsPage::importFinished() +{ + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + + if (wizard()->downloadedPlainXml || watcher.future().result()) { + wizard()->next(); + } else { + QMessageBox::critical(this, tr("Error"), + tr("The file was retrieved successfully, but it does not contain any sets data.")); + } +} + +SaveSetsPage::SaveSetsPage(QWidget *parent) : OracleWizardPage(parent) +{ + pathLabel = new QLabel(this); + saveLabel = new QLabel(this); + + defaultPathCheckBox = new QCheckBox(this); + + messageLog = new QTextEdit(this); + messageLog->setReadOnly(true); + + auto *layout = new QGridLayout(this); + layout->addWidget(messageLog, 0, 0); + layout->addWidget(saveLabel, 1, 0); + layout->addWidget(pathLabel, 2, 0); + layout->addWidget(defaultPathCheckBox, 3, 0); + + setLayout(layout); +} + +void SaveSetsPage::cleanupPage() +{ + wizard()->importer->clear(); + disconnect(wizard()->importer, &OracleImporter::setIndexChanged, nullptr, nullptr); +} + +void SaveSetsPage::initializePage() +{ + messageLog->clear(); + + retranslateUi(); + if (wizard()->downloadedPlainXml) { + messageLog->hide(); + } else { + messageLog->show(); + connect(wizard()->importer, &OracleImporter::setIndexChanged, this, &SaveSetsPage::updateTotalProgress); + + int setsImported = wizard()->importer->startImport(); + + if (setsImported == 0) { + QMessageBox::critical(this, tr("Error"), tr("No set has been imported.")); + } + } + + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +void SaveSetsPage::retranslateUi() +{ + setTitle(tr("Sets imported")); + if (wizard()->downloadedPlainXml) { + setSubTitle(tr("A cockatrice database file of %1 MB has been downloaded.") + .arg(qRound(wizard()->xmlData.size() / 1000000.0))); + } else { + setSubTitle(tr("The following sets have been found:")); + } + + saveLabel->setText(tr("Press \"Save\" to store the imported cards in the Cockatrice database.")); + pathLabel->setText(tr("The card database will be saved at the following location:") + "
" + + SettingsCache::instance().getCardDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); + + setButtonText(QWizard::NextButton, tr("&Save")); +} + +void SaveSetsPage::updateTotalProgress(int cardsImported, int /* setIndex */, const QString &setName) +{ + if (setName.isEmpty()) { + messageLog->append("" + tr("Import finished: %1 cards.").arg(wizard()->importer->getCardList().size()) + + ""); + } else { + messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported)); + } + + messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum()); +} + +bool SaveSetsPage::validatePage() +{ + QString defaultPath = SettingsCache::instance().getCardDatabasePath(); + QString windowName = tr("Save card database"); + QString fileType = tr("XML; card database (*.xml)"); + + QString fileName; + if (defaultPathCheckBox->isChecked()) { + fileName = QFileDialog::getSaveFileName(this, windowName, defaultPath, fileType); + } else { + fileName = defaultPath; + } + + if (fileName.isEmpty()) { + return false; + } + + QFileInfo fi(fileName); + QDir fileDir(fi.path()); + if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) { + return false; + } + + if (wizard()->downloadedPlainXml) { + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + if (file.write(wizard()->xmlData) < 1) { + qDebug() << "File write (w) failed for" << fileName; + return false; + } + wizard()->xmlData.clear(); + } else 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; +} + +void LoadTokensPage::initializePage() +{ + SimpleDownloadFilePage::initializePage(); + + if (wizard()->backgroundMode) { + emit readyToContinue(); + } +} + +QString LoadTokensPage::getDefaultUrl() +{ + return TOKENS_URL; +} + +QString LoadTokensPage::getCustomUrlSettingsKey() +{ + return "tokensurl"; +} + +QString LoadTokensPage::getDefaultSavePath() +{ + return SettingsCache::instance().getTokenDatabasePath(); +} + +QString LoadTokensPage::getWindowTitle() +{ + return tr("Save token database"); +} + +QString LoadTokensPage::getFileType() +{ + return tr("XML; token database (*.xml)"); +} + +QString LoadTokensPage::getFilePromptName() +{ + return tr("tokens"); +} + +void LoadTokensPage::retranslateUi() +{ + setTitle(tr("Tokens import")); + setSubTitle(tr("Please specify a compatible source for token data.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); + + pathLabel->setText(tr("The token database will be saved at the following location:") + "
" + + SettingsCache::instance().getTokenDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); +} + +QString LoadSpoilersPage::getDefaultUrl() +{ + return SPOILERS_URL; +} + +QString LoadSpoilersPage::getCustomUrlSettingsKey() +{ + return "spoilersurl"; +} + +QString LoadSpoilersPage::getDefaultSavePath() +{ + return SettingsCache::instance().getTokenDatabasePath(); +} + +QString LoadSpoilersPage::getWindowTitle() +{ + return tr("Save spoiler database"); +} + +QString LoadSpoilersPage::getFileType() +{ + return tr("XML; spoiler database (*.xml)"); +} + +QString LoadSpoilersPage::getFilePromptName() +{ + return tr("spoiler"); +} + +void LoadSpoilersPage::retranslateUi() +{ + setTitle(tr("Spoilers import")); + setSubTitle(tr("Please specify a compatible source for spoiler data.")); + + urlRadioButton->setText(tr("Download URL:")); + fileRadioButton->setText(tr("Local file:")); + urlButton->setText(tr("Restore default URL")); + fileButton->setText(tr("Choose file...")); + + pathLabel->setText(tr("The spoiler database will be saved at the following location:") + "
" + + SettingsCache::instance().getSpoilerCardDatabasePath()); + defaultPathCheckBox->setText(tr("Save to a custom path (not recommended)")); +} \ No newline at end of file diff --git a/oracle/src/pages.h b/oracle/src/pages.h new file mode 100644 index 000000000..066cc2e1b --- /dev/null +++ b/oracle/src/pages.h @@ -0,0 +1,156 @@ +#ifndef COCKATRICE_PAGES_H +#define COCKATRICE_PAGES_H + +#include "pagetemplates.h" + +#include +#include +#include +#include +#include + +class QCheckBox; +class QGroupBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QRadioButton; +class QProgressBar; +class QNetworkAccessManager; +class QTextEdit; +class QVBoxLayout; +class OracleImporter; +class QSettings; + +class IntroPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit IntroPage(QWidget *parent = nullptr); + void retranslateUi() override; + +private: + QStringList findQmFiles(); + QString languageName(const QString &lang); + +private: + QLabel *label, *languageLabel, *versionLabel; + QComboBox *languageBox; + +private slots: + void languageBoxChanged(int index); + +protected slots: + void initializePage() override; +}; + +class OutroPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit OutroPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + void initializePage() override; +}; + +class LoadSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit LoadSetsPage(QWidget *parent = nullptr); + void retranslateUi() override; + +protected: + void initializePage() override; + bool validatePage() override; + void readSetsFromByteArray(QByteArray _data); + void readSetsFromByteArrayRef(QByteArray &_data); + void downloadSetsFile(const QUrl &url); + +private: + QRadioButton *urlRadioButton; + QRadioButton *fileRadioButton; + QLineEdit *urlLineEdit; + QLineEdit *fileLineEdit; + QPushButton *urlButton; + QPushButton *fileButton; + QLabel *progressLabel; + QProgressBar *progressBar; + + QFutureWatcher watcher; + QFuture future; + QByteArray jsonData; + +private slots: + void actLoadSetsFile(); + void actRestoreDefaultUrl(); + void actDownloadProgressSetsFile(qint64 received, qint64 total); + void actDownloadFinishedSetsFile(); + void importFinished(); + void zipDownloadFailed(const QString &message); +}; + +class SaveSetsPage : public OracleWizardPage +{ + Q_OBJECT +public: + explicit SaveSetsPage(QWidget *parent = nullptr); + void retranslateUi() override; + +private: + QTextEdit *messageLog; + QCheckBox *defaultPathCheckBox; + QLabel *pathLabel; + QLabel *saveLabel; + +protected: + void initializePage() override; + void cleanupPage() override; + bool validatePage() override; + +private slots: + void updateTotalProgress(int cardsImported, int setIndex, const QString &setName); +}; + +class LoadSpoilersPage : public SimpleDownloadFilePage +{ + Q_OBJECT +public: + explicit LoadSpoilersPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + QString getDefaultUrl() override; + QString getCustomUrlSettingsKey() override; + QString getDefaultSavePath() override; + QString getWindowTitle() override; + QString getFileType() override; + QString getFilePromptName() override; +}; + +class LoadTokensPage : public SimpleDownloadFilePage +{ + Q_OBJECT +public: + explicit LoadTokensPage(QWidget * = nullptr) + { + } + void retranslateUi() override; + +protected: + QString getDefaultUrl() override; + QString getCustomUrlSettingsKey() override; + QString getDefaultSavePath() override; + QString getWindowTitle() override; + QString getFileType() override; + QString getFilePromptName() override; + void initializePage() override; +}; + +#endif // COCKATRICE_PAGES_H diff --git a/oracle/src/pagetemplates.cpp b/oracle/src/pagetemplates.cpp index d9ce3300f..4a27fef30 100644 --- a/oracle/src/pagetemplates.cpp +++ b/oracle/src/pagetemplates.cpp @@ -12,32 +12,43 @@ #include #include #include +#include #include SimpleDownloadFilePage::SimpleDownloadFilePage(QWidget *parent) : OracleWizardPage(parent) { - urlLabel = new QLabel(this); + urlRadioButton = new QRadioButton(this); + fileRadioButton = new QRadioButton(this); + urlLineEdit = new QLineEdit(this); + fileLineEdit = new QLineEdit(this); progressLabel = new QLabel(this); progressBar = new QProgressBar(this); + urlRadioButton->setChecked(true); + urlButton = new QPushButton(this); connect(urlButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actRestoreDefaultUrl); - defaultPathCheckBox = new QCheckBox(this); + fileButton = new QPushButton(this); + connect(fileButton, &QPushButton::clicked, this, &SimpleDownloadFilePage::actLoadCardFile); + defaultPathCheckBox = new QCheckBox(this); pathLabel = new QLabel(this); pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); auto *layout = new QGridLayout(this); - layout->addWidget(urlLabel, 0, 0); + layout->addWidget(urlRadioButton, 0, 0); layout->addWidget(urlLineEdit, 0, 1); layout->addWidget(urlButton, 1, 1, Qt::AlignRight); - layout->addWidget(pathLabel, 2, 0, 1, 2); - layout->addWidget(defaultPathCheckBox, 3, 0, 1, 2); - layout->addWidget(progressLabel, 4, 0); - layout->addWidget(progressBar, 4, 1); + layout->addWidget(fileRadioButton, 2, 0); + layout->addWidget(fileLineEdit, 2, 1); + layout->addWidget(fileButton, 3, 1, Qt::AlignRight); + layout->addWidget(pathLabel, 4, 0, 1, 2); + layout->addWidget(defaultPathCheckBox, 5, 0, 1, 2); + layout->addWidget(progressLabel, 6, 0); + layout->addWidget(progressBar, 6, 1); setLayout(layout); } @@ -56,6 +67,31 @@ void SimpleDownloadFilePage::actRestoreDefaultUrl() urlLineEdit->setText(getDefaultUrl()); } +void SimpleDownloadFilePage::actLoadCardFile() +{ + QFileDialog dialog(this, tr("Load %1 file").arg(getFilePromptName())); + dialog.setFileMode(QFileDialog::ExistingFile); + + QString extensions = "*.json *.xml"; +#ifdef HAS_ZLIB + extensions += " *.zip"; +#endif +#ifdef HAS_LZMA + extensions += " *.xz"; +#endif + dialog.setNameFilter(tr("%1 file (%1)").arg(getFilePromptName(), extensions)); + + if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { + dialog.selectFile(fileLineEdit->text()); + } + + if (!dialog.exec()) { + return; + } + + fileLineEdit->setText(dialog.selectedFiles().at(0)); +} + bool SimpleDownloadFilePage::validatePage() { // if data has already been downloaded, pass directly to the "save" step @@ -68,22 +104,41 @@ bool SimpleDownloadFilePage::validatePage() } } - QUrl url = QUrl::fromUserInput(urlLineEdit->text()); - if (!url.isValid()) { - QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString()); - return false; + // else, try to import sets + if (urlRadioButton->isChecked()) { + QUrl url = QUrl::fromUserInput(urlLineEdit->text()); + if (!url.isValid()) { + QMessageBox::critical(this, tr("Error"), tr("The provided URL is not valid: ") + url.toString()); + return false; + } + + progressLabel->setText(tr("Downloading (0MB)")); + // show an infinite progressbar + progressBar->setMaximum(0); + progressBar->setMinimum(0); + progressBar->setValue(0); + progressLabel->show(); + progressBar->show(); + + wizard()->disableButtons(); + downloadFile(url); + + } else if (fileRadioButton->isChecked()) { + QFile cardFile(fileLineEdit->text()); + if (!cardFile.exists()) { + QMessageBox::critical(this, tr("Error"), tr("Please choose a file.")); + return false; + } + + if (!cardFile.open(QIODevice::ReadOnly)) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open file '%1'.").arg(fileLineEdit->text())); + return false; + } + + downloadData = cardFile.readAll(); + wizard()->next(); } - progressLabel->setText(tr("Downloading (0MB)")); - // show an infinite progressbar - progressBar->setMaximum(0); - progressBar->setMinimum(0); - progressBar->setValue(0); - progressLabel->show(); - progressBar->show(); - - wizard()->disableButtons(); - downloadFile(url); return false; } diff --git a/oracle/src/pagetemplates.h b/oracle/src/pagetemplates.h index 874652d17..79dcdd632 100644 --- a/oracle/src/pagetemplates.h +++ b/oracle/src/pagetemplates.h @@ -3,6 +3,8 @@ #include +class QFile; +class QRadioButton; class OracleWizard; class QCheckBox; class QLabel; @@ -13,7 +15,9 @@ class OracleWizardPage : public QWizardPage { Q_OBJECT public: - explicit OracleWizardPage(QWidget *parent = nullptr) : QWizardPage(parent){}; + explicit OracleWizardPage(QWidget *parent = nullptr) : QWizardPage(parent) + { + } virtual void retranslateUi() = 0; signals: @@ -41,15 +45,19 @@ protected: virtual QString getDefaultSavePath() = 0; virtual QString getWindowTitle() = 0; virtual QString getFileType() = 0; + virtual QString getFilePromptName() = 0; bool saveToFile(); bool internalSaveToFile(const QString &fileName); protected: QByteArray downloadData; - QLabel *urlLabel; - QLabel *pathLabel; + QRadioButton *urlRadioButton; + QRadioButton *fileRadioButton; QLineEdit *urlLineEdit; + QLineEdit *fileLineEdit; QPushButton *urlButton; + QPushButton *fileButton; + QLabel *pathLabel; QLabel *progressLabel; QProgressBar *progressBar; QCheckBox *defaultPathCheckBox; @@ -58,6 +66,7 @@ signals: void parsedDataReady(); private slots: void actRestoreDefaultUrl(); + void actLoadCardFile(); void actDownloadProgress(qint64 received, qint64 total); void actDownloadFinished(); }; diff --git a/oracle/src/zip/unzip.cpp b/oracle/src/zip/unzip.cpp index 1e5910051..8e52ece18 100755 --- a/oracle/src/zip/unzip.cpp +++ b/oracle/src/zip/unzip.cpp @@ -245,7 +245,6 @@ UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev) \internal Parses a local header record and makes some consistency check with the information stored in the Central Directory record for this entry that has been previously parsed. - \todo Optional consistency check (as a ExtractionOptions flag) local file header signature 4 bytes (0x04034b50) version needed to extract 2 bytes @@ -262,6 +261,7 @@ UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev) file name (variable size) extra field (variable size) */ +//! \todo Optional consistency check (as a ExtractionOptions flag) UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry) { Q_ASSERT(device); diff --git a/oracle/translations/oracle_de.ts b/oracle/translations/oracle_de.ts index cce06388a..df12f40f4 100644 --- a/oracle/translations/oracle_de.ts +++ b/oracle/translations/oracle_de.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Einführung - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Dieser Assistent wird eine Liste aller Editionen, Karten und Spielsteine importieren, die von Cockatrice genutzt werden. - + Interface language: Interfacesprache: - + Version: Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Quellenauswahl - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Bitte geben Sie eine kompatible Quelle für die Liste der Editionen und Karten an. Sie können eine URL-Adresse zum Herunterladen angeben oder eine existierende Datei von Ihrem Computer verwenden. - + Download URL: Download URL: - + Local file: Lokale Datei: - + Restore default URL Standard-URL wiederherstellen - + Choose file... Datei auswählen... - + Load sets file Editionsdatei wird geladen - + Sets file (%1) Sets JSON file (%1) Sets Datei (%1) - - - - - - - + + + + + + + Error Fehler - + The provided URL is not valid. Die eingegebene URL ist nicht gültig. - + Downloading (0MB) Herunterladen (0MB) - + Please choose a file. - Bitte wählen Sie eine Datei. + Bitte wähle eine Datei aus. - + Cannot open file '%1'. Datei '%1' kann nicht geöffnet werden. - + Downloading (%1MB) Herunterladen (%1MB) - + Network error: %1. Netzwerkfehler: %1. - + Parsing file Datei wird verarbeitet - + Xz extraction failed. Fehler beim Extrahieren der xz Datei. - + Sorry, this version of Oracle does not support xz compressed files. Es tut uns Leid, diese Version von Oracle unterstützt keine xz komprimierten Dateien. - + Failed to open Zip archive: %1. Fehler beim Öffnen des Zip Archivs: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Fehler beim Extrahieren: Das Zip Archiv enthält mehr als eine Datei. - + Zip extraction failed: %1. Fehler beim Extrahieren: %1. - + Sorry, this version of Oracle does not support zipped files. Es tut uns Leid, diese Version von Oracle unterstützt keine Zip Archive. - + Failed to interpret downloaded data. Interpretation der heruntergeladenen Daten fehlgeschlagen. - + Do you want to download the uncompressed file instead? Möchten Sie stattdessen die unkomprimierte Datei herunterladen? - + The file was retrieved successfully, but it does not contain any sets data. Die Datei wurde erfolgreich abgerufen, sie enthält aber keine Editionsdaten. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Speichere Spoilerdatenbank - + XML; spoiler database (*.xml) XML; Spoilerdatenbank (*.xml) - + + spoiler + Spoiler + + + Spoilers import Spoilerimport - + Please specify a compatible source for spoiler data. Bitte geben Sie eine kompatible Quelle für Spoilerdaten an. - + Download URL: Download URL: - + + Local file: + Lokale Datei: + + + Restore default URL Standard-URL wiederherstellen - + + Choose file... + Datei auswählen... + + + The spoiler database will be saved at the following location: Die Spoilerdatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Speichere Spielsteindatenbank - + XML; token database (*.xml) XML; Spielsteindatenbank (*.xml) - + + tokens + Spielsteine + + + Tokens import Spielsteinimport - + Please specify a compatible source for token data. Bitte geben Sie eine kompatible Quelle für Spielsteindaten an. - + Download URL: Download URL: - + + Local file: + Lokale Datei: + + + Restore default URL Standard-URL wiederherstellen - + + Choose file... + Datei auswählen... + + + The token database will be saved at the following location: Die Spielsteindatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Platzhalter Edition mit Spielsteinen @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle Importer @@ -262,22 +292,22 @@ OutroPage - + Finished Fertig - + The wizard has finished. Der Wizard ist fertig. - + You can now start using Cockatrice with the newly updated cards. Sie können nun Cockatrice mit den aktuellen Karten verwenden. - + If the card databases don't reload automatically, restart the Cockatrice client. Falls die Datenbanken nicht automatisch neu geladen werden, starten Sie bitte Cockatrice neu. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Fehler - + No set has been imported. Es wurden keine Editionen importiert. - + Sets imported Editionen wurden importiert - + A cockatrice database file of %1 MB has been downloaded. Eine Cockatrice-Datenbankdatei der Größe %1 MB wurde heruntergeladen. - + The following sets have been found: Die folgenden Sets wurden gefunden: - + Press "Save" to store the imported cards in the Cockatrice database. Drücken Sie "Speichern", um die importierten Karten in der Datenbank zu speichern. - + The card database will be saved at the following location: Die Kartendatenbank wird in folgendem Pfad gespeichert: - + Save to a custom path (not recommended) Speichere in benutzerdefiniertem Pfad (nicht empfohlen) - + &Save &Speichern - + Import finished: %1 cards. Importieren abgeschlossen: %1 Karten. - + %1: %2 cards imported %1: %2 Karten importiert. - + Save card database Kartendatenbank speichern - + XML; card database (*.xml) XML; Kartendatenbank (*.xml) - + The file could not be saved to %1 Die Datei konnte nicht gespeichert werden: %1 @@ -360,34 +390,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + Lade %1 Datei + + + + %1 file (%1) + %1 Datei (%1) + + + + + + + Error Fehler - + The provided URL is not valid: Die bereitgestellte URL ist nicht gültig: - + Downloading (0MB) Herunterladen (0MB) - + + Please choose a file. + Bitte wähle eine Datei aus. + + + + Cannot open file '%1'. + Datei '%1' kann nicht geöffnet werden. + + + Downloading (%1MB) Herunterladen (%1MB) - + Network error: %1. Netzwerkfehler: %1. - + The file could not be saved to %1 Die Datei konnte nicht in %1 gespeichert werden @@ -536,7 +588,7 @@ i18n - + English Deutsch (German) diff --git a/oracle/translations/oracle_el.ts b/oracle/translations/oracle_el.ts index 03941de8d..7934997cf 100644 --- a/oracle/translations/oracle_el.ts +++ b/oracle/translations/oracle_el.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Εισαγωγή - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Αυτός ο αυτόματος οδηγός θα εισάγει τη λίστα των σετ, καρτών και δειγμάτων (tokens) που θα χρησιμοποιηθούν από το Cockatrice. - + Interface language: - + Version: Έκδοση: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Επιλογή πηγής - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: URL λήψης: - + Local file: Τοπικό αρχείο: - + Restore default URL Επαναφορά προκαθορισμένης διεύθυνσης URL - + Choose file... Επιλέξτε αρχείο... - + Load sets file Φόρτωση αρχείων - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Σφάλμα - + The provided URL is not valid. Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. - + Downloading (0MB) Λήψη (0MB) - + Please choose a file. Παρακαλώ διαλέξτε έναν αρχείο. - + Cannot open file '%1'. Δεν είναι δυνατό να ανοίξει το αρχείο '% 1'. - + Downloading (%1MB) Λήψη (% 1MB) - + Network error: %1. Σφάλμα δικτύου: % 1. - + Parsing file Ανάλυση αρχείου - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. Αποτυχία ανοίγματος αρχείου Zip:% 1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Η εξαγωγή zip απέτυχε: το αρχείο Zip δεν περιέχει ακριβώς ένα αρχείο. - + Zip extraction failed: %1. Η εξαγωγή zip απέτυχε: % 1. - + Sorry, this version of Oracle does not support zipped files. Λυπούμαστε, αυτή η έκδοση του Oracle δεν υποστηρίζει αρχεία τύπου zipped. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. Το αρχείο ανακτήθηκε με επιτυχία, αλλά δεν περιέχει σύνολα δεδομένων. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: URL λήψης: - + + Local file: + + + + Restore default URL Επαναφορά προκαθορισμένης διεύθυνσης URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Εικονικό σετ που περιέχει tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Εισαγωγέας Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Σφάλμα - + No set has been imported. Δεν έχει εισαχθεί κανένα σετ. - + Sets imported Εισαγόμενα σετς - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. Η εισαγωγή ολοκληρώθηκε: % 1 κάρτες. - + %1: %2 cards imported %1: %2 κάρτες εισήχθησαν - + Save card database Αποθήκευση κάρτας βάσης δεδομένων - + XML; card database (*.xml) XML; κάρτα βάσης δεδομένων (* .xml) - + The file could not be saved to %1 Δεν ήταν δυνατή η αποθήκευση του αρχείου στο %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + Η λειτουργία ZIP ολοκληρώθηκε με επιτυχία. Failed to initialize or load zlib library. - + Δεν ήταν δυνατή η προετοιμασία ή η φόρτωση της βιβλιοθήκης zlib. zlib library error. - + Σφάλμα βιβλιοθήκης zlib. Unable to create or open file. - + Δεν είναι δυνατή η δημιουργία ή το άνοιγμα αρχείου. Partially corrupted archive. Some files might be extracted. - + Μερικώς κατεστραμμένο αρχείο. Κάποια αρχεία ενδέχεται να εξαχθούν. Corrupted archive. - + Κατεστραμμένο αρχείο. Wrong password. - + Λάθος κωδικός. No archive has been created yet. - + Δεν έχει δημιουργηθεί κανένα αρχείο ακόμα. File or directory does not exist. - + Το αρχείο ή ο κατάλογος δεν υπάρχει. File read error. - + Σφάλμα ανάγνωσης αρχείου. File write error. - + Σφάλμα εγγραφής αρχείου. File seek error. - + Σφάλμα αναζήτησης αρχείου. Unable to create a directory. - + Δεν είναι δυνατή η δημιουργία ενός καταλόγου. Invalid device. - + Μη έγκυρη συσκευή. Invalid or incompatible zip archive. - + Μη έγκυρο ή ασυμβίβαστο zip αρχείο. Inconsistent headers. Archive might be corrupted. - + Αντιφατικές κεφαλίδες. Το αρχείο μπορεί να είναι κατεστραμμένο. Unknown error. - + Άγνωστο σφάλμα. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + Η λειτουργία ZIP ολοκληρώθηκε με επιτυχία. Failed to initialize or load zlib library. - + Δεν ήταν δυνατή η προετοιμασία ή η φόρτωση της βιβλιοθήκης zlib. zlib library error. - + Σφάλμα βιβλιοθήκης zlib. Unable to create or open file. - + Δεν είναι δυνατή η δημιουργία ή το άνοιγμα αρχείου. No archive has been created yet. - + Δεν έχει δημιουργηθεί αρχείο ακόμα. File or directory does not exist. - + Το αρχείο ή ο κατάλογος δεν υπάρχει. File read error. - + Σφάλμα ανάγνωσης αρχείου. File write error. - + Σφάλμα εγγραφής αρχείου. File seek error. - + Σφάλμα αναζήτησης αρχείου. Unknown error. - + Άγνωστο σφάλμα. i18n - + English Ελληνικά (Greek) diff --git a/oracle/translations/oracle_en@pirate.ts b/oracle/translations/oracle_en@pirate.ts index 42b3d1273..13a57750b 100644 --- a/oracle/translations/oracle_en@pirate.ts +++ b/oracle/translations/oracle_en@pirate.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -25,133 +25,134 @@ LoadSetsPage - + Source selection - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: - + Local file: - + Restore default URL - + Choose file... - + Load sets file - - Sets JSON file (%1) + + Sets file (%1) + Sets JSON file (%1) - - - - - - - + + + + + + + Error Cap'n? Thar be a problem - + The provided URL is not valid. - + Downloading (0MB) - + Please choose a file. - + Cannot open file '%1'. - + Downloading (%1MB) - + Network error: %1. Cap'n? Thar be a problem wi' t' smoke signals: %1. - + Parsing file - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. @@ -159,42 +160,42 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + Restore default URL - + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -202,42 +203,42 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + Tokens import - + Please specify a compatible source for token data. - + Download URL: - + Restore default URL - + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -245,7 +246,7 @@ OracleImporter - + Dummy set containing tokens @@ -253,7 +254,7 @@ OracleWizard - + Oracle Importer @@ -261,22 +262,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -284,73 +285,73 @@ SaveSetsPage - - + + Error Cap'n? Thar be a problem - + No set has been imported. - + Sets imported - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + The file could not be saved to %1 @@ -366,7 +367,7 @@ - The provided URL is not valid. + The provided URL is not valid: @@ -405,7 +406,7 @@ zlib library error. - + Cap'n? Thar be a problem wi' t' zlib library. @@ -475,7 +476,7 @@ Unknown error. - + Cap'n? Thar be a unknown problem wi' t' ship. @@ -493,7 +494,7 @@ zlib library error. - + Cap'n? Thar be a problem wi' t' zlib library. @@ -528,7 +529,7 @@ Unknown error. - + Cap'n? Thar be a unknown problem wi' t' ship. @@ -542,9 +543,14 @@ main - + Only run in spoiler mode + + + Run in no-confirm background mode + + \ No newline at end of file diff --git a/oracle/translations/oracle_en_US.ts b/oracle/translations/oracle_en_US.ts index bdaafafa0..4b664bf57 100644 --- a/oracle/translations/oracle_en_US.ts +++ b/oracle/translations/oracle_en_US.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: Interface language: - + Version: Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Source selection - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: Download URL: - + Local file: Local file: - + Restore default URL Restore default URL - + Choose file... Choose file... - + Load sets file Load sets file - + Sets file (%1) Sets JSON file (%1) - + Sets file (%1) - - - - - - - + + + + + + + Error Error - + The provided URL is not valid. The provided URL is not valid. - + Downloading (0MB) Downloading (0MB) - + Please choose a file. Please choose a file. - + Cannot open file '%1'. Cannot open file '%1'. - + Downloading (%1MB) Downloading (%1MB) - + Network error: %1. Network error: %1. - + Parsing file Parsing file - + Xz extraction failed. Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. Failed to open Zip archive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. Sorry, this version of Oracle does not support zipped files. - + Failed to interpret downloaded data. Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. The file was retrieved successfully, but it does not contain any sets data. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Save spoiler database - + XML; spoiler database (*.xml) XML; spoiler database (*.xml) - + + spoiler + spoiler + + + Spoilers import Spoilers import - + Please specify a compatible source for spoiler data. Please specify a compatible source for spoiler data. - + Download URL: Download URL: - + + Local file: + Local file: + + + Restore default URL Restore default URL - + + Choose file... + Choose file... + + + The spoiler database will be saved at the following location: The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Save token database - + XML; token database (*.xml) XML; token database (*.xml) - + + tokens + tokens + + + Tokens import Tokens import - + Please specify a compatible source for token data. Please specify a compatible source for token data. - + Download URL: Download URL: - + + Local file: + Local file: + + + Restore default URL Restore default URL - + + Choose file... + Choose file... + + + The token database will be saved at the following location: The token database will be saved at the following location: - + Save to a custom path (not recommended) Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Dummy set containing tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle Importer @@ -262,22 +292,22 @@ OutroPage - + Finished Finished - + The wizard has finished. The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Error - + No set has been imported. No set has been imported. - + Sets imported Sets imported - + A cockatrice database file of %1 MB has been downloaded. A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: The card database will be saved at the following location: - + Save to a custom path (not recommended) Save to a custom path (not recommended) - + &Save &Save - + Import finished: %1 cards. Import finished: %1 cards. - + %1: %2 cards imported %1: %2 cards imported - + Save card database Save card database - + XML; card database (*.xml) XML; card database (*.xml) - + The file could not be saved to %1 The file could not be saved to %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + Load %1 file + + + + %1 file (%1) + %1 file (%1) + + + + + + + Error Error - + The provided URL is not valid: - + The provided URL is not valid: - + Downloading (0MB) Downloading (0MB) - + + Please choose a file. + Please choose a file. + + + + Cannot open file '%1'. + Cannot open file '%1'. + + + Downloading (%1MB) Downloading (%1MB) - + Network error: %1. Network error: %1. - + The file could not be saved to %1 The file could not be saved to %1 @@ -535,7 +587,7 @@ i18n - + English English @@ -550,7 +602,7 @@ Run in no-confirm background mode - + Run in no-confirm background mode \ No newline at end of file diff --git a/oracle/translations/oracle_es.ts b/oracle/translations/oracle_es.ts index f97075a93..eeb9f71bd 100644 --- a/oracle/translations/oracle_es.ts +++ b/oracle/translations/oracle_es.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introducción - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Este instalador importará la lista de ediciones, cartas y fichas que va a usar Cockatrice. - + Interface language: Idioma de la interfaz: - + Version: Versión: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Seleccionar origen - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Por favor especifica un origen compatible para la lista de sets y cartas. Puedes especificar la URL de donde descargarla o usar un archivo existente de tu ordenador. - + Download URL: URL de descarga: - + Local file: Archivo local: - + Restore default URL Restablecer URL predeterminada - + Choose file... Elegir archivo... - + Load sets file Cargar archivo de ediciones - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Error - + The provided URL is not valid. La URL suministrada no es válida. - + Downloading (0MB) Descargando (0MB) - + Please choose a file. Por favor elija un archivo. - + Cannot open file '%1'. No se puede abrir el archivo '%1' - + Downloading (%1MB) Descargando (%1MB) - + Network error: %1. Error de red: %1 - + Parsing file Procesando archivo - + Xz extraction failed. Extracción de Xz fallida - + Sorry, this version of Oracle does not support xz compressed files. Lo sentimos, esta versión de Oracle no soporta archivos comprimidos xz - + Failed to open Zip archive: %1. Error al abrir el archivo Zip: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Fallo al extraer el contenido: el Zip contiene más de un archivo. - + Zip extraction failed: %1. Error al extraer el contenido del Zip: %1. - + Sorry, this version of Oracle does not support zipped files. Lo sentimos, esta versión de Oracle no soporta archivos comprimidos. - + Failed to interpret downloaded data. No se pudieron interpretar los datos descargados. - + Do you want to download the uncompressed file instead? ¿Prefieres descargar el archivo sin comprimir? - + The file was retrieved successfully, but it does not contain any sets data. El archivo fue cargado correctamente pero no contiene datos sobre ningún set. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Guardar la base de datos de spoilers - + XML; spoiler database (*.xml) XML; base de datos de spoilers (*.xml) - + + spoiler + + + + Spoilers import Importar spoilers - + Please specify a compatible source for spoiler data. Por favor elija un origen compatible para los datos de los spoilers. - + Download URL: URL de descarga: - + + Local file: + + + + Restore default URL Restaurar URL por defecto - + + Choose file... + + + + The spoiler database will be saved at the following location: La base de datos de los spoilers será guardada en la siguiente ubicación: - + Save to a custom path (not recommended) Guardar en una ruta personalizada (no recomendado) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Guardar base de datos de fichas - + XML; token database (*.xml) XML; base de datos de fichas (*.xml) - + + tokens + + + + Tokens import Importar tokens - + Please specify a compatible source for token data. Por favor elija un origen compatible para los datos de las fichas. - + Download URL: URL de descarga: - + + Local file: + + + + Restore default URL Restablecer URL predeterminada - + + Choose file... + + + + The token database will be saved at the following location: La base de datos de las fichas será guardada en la siguiente ubicación: - + Save to a custom path (not recommended) Guardar en una ruta personalizada (no recomendado) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Set dedicado para tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Importador de Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished Completado - + The wizard has finished. El asistente ha terminado. - + You can now start using Cockatrice with the newly updated cards. Ahora puedes usar Cockatrice con las cartas actualizadas. - + If the card databases don't reload automatically, restart the Cockatrice client. Si las bases de datos de cartas no se recargan automáticamente, reinicia el cliente de Cockatrice. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Error - + No set has been imported. Ningún set ha sido importado. - + Sets imported Sets importados - + A cockatrice database file of %1 MB has been downloaded. Se ha descargado un archivo de base de datos cockatrice de %1 MB. - + The following sets have been found: Las siguientes ediciones han sido encontradas: - + Press "Save" to store the imported cards in the Cockatrice database. Pulsa "Guardar" para guardar las cartas importadas en la base de datos de Cockatrice. - + The card database will be saved at the following location: La base de datos de cartas ha sido guardada en la siguiente ubicación: - + Save to a custom path (not recommended) Guardar en una ruta personalizada (no recomendado) - + &Save &Guardar - + Import finished: %1 cards. Importación terminada: %1 cartas. - + %1: %2 cards imported %1: %2 cartas importadas - + Save card database Guardar base de datos de cartas - + XML; card database (*.xml) XML; base de datos de cartas (*.xml) - + The file could not be saved to %1 El archivo no ha podido ser guardado en %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Error - + The provided URL is not valid: - + La URL proporcionada no es válida: - + Downloading (0MB) Descargando (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Descargando (%1MB) - + Network error: %1. Error de red: %1. - + The file could not be saved to %1 El archivo no ha podido ser guardado en %1 @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + La operación ZIP se completó con éxito. Failed to initialize or load zlib library. - + Fallo al iniciar o cargar la librería zlib. zlib library error. - + Error en la librería zlib. Unable to create or open file. - + No es posible crear o abrir el archivo. No archive has been created yet. - + No se ha creado ningún archivo todavía. File or directory does not exist. - + El archivo o directorio no existe. File read error. - + Error al leer el archivo. File write error. - + Error al escribir en archivo. File seek error. - + Error al buscar en el archivo. Unknown error. - + Error desconocido. i18n - + English Español (Spanish) diff --git a/oracle/translations/oracle_et.ts b/oracle/translations/oracle_et.ts index a03cc4da8..9f2c560cd 100644 --- a/oracle/translations/oracle_et.ts +++ b/oracle/translations/oracle_et.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Sissejuhatus - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. See võlur impordib Cockatrice’is kasutatava nimekirja komplektidest, kaartidest ja märgistustest. - + Interface language: - + Version: Versioon: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Allika valik - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Palun täpsusta ühilduv allikas komplektide ja kaartide nimekirja jaoks. Võid täpsustada URL-i aadressi, mis laaditakse alla või kasutada olemasolevat faili oma arvutist. - + Download URL: Allalaadimise URL: - + Local file: Fail arvutis: - + Restore default URL Taasta tavaURL - + Choose file... Valige fail.... - + Load sets file Lae komplektide kaust - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Viga - + The provided URL is not valid. Antud URl pole kehtiv. - + Downloading (0MB) Allalaadimine (0MB) - + Please choose a file. Palun valige fail. - + Cannot open file '%1'. Ei suudeta avada '%1'. - + Downloading (%1MB) Allalaadimine (%1MB) - + Network error: %1. Võrgu viga: %1. - + Parsing file Faili hankimine - + Xz extraction failed. Xzi lahtipakkimine nurjus. - + Sorry, this version of Oracle does not support xz compressed files. Vabandame, aga antud Oracle’i versioon ei toeta xz kokkupakitud faile. - + Failed to open Zip archive: %1. Zip-arhiivi avamine ebaõnnestus: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Zip-i lahtipakkimine ebaõnnestus: Zip-arhiiv sisaldab rohkem faile kui üks. - + Zip extraction failed: %1. Zipi lahtipakkimine ebaõnnestus: %1. - + Sorry, this version of Oracle does not support zipped files. Vabandame, aga antud Oracle versioon ei toeta kokkupakitud faile. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? Soovid alla laadida hoopis pakkimata faili? - + The file was retrieved successfully, but it does not contain any sets data. Fail on edukalt alla laetud, ent ei sisalda andmeid. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Salvesta spoileri andmebaas - + XML; spoiler database (*.xml) XML; spoileri andmebaas (*.xml) - + + spoiler + + + + Spoilers import Spoilerite import - + Please specify a compatible source for spoiler data. Täpsusta spoileri andmetega ühilduv allikas. - + Download URL: Allalaadimise URL: - + + Local file: + + + + Restore default URL Taasta tavaURL - + + Choose file... + + + + The spoiler database will be saved at the following location: Spoileri andmebaas salvestatakse järgmisesse asukohta: - + Save to a custom path (not recommended) Salvesta enda määratud asukohta (pole soovitatav) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Salvesta märgistuste andmebaas - + XML; token database (*.xml) XML; märgistuste andmebaas (*.xml) - + + tokens + + + + Tokens import Märgistuste importimine - + Please specify a compatible source for token data. Täpsusta märgistuste andmetega ühilduv allikas. - + Download URL: Allalaadimise URL: - + + Local file: + + + + Restore default URL Taasta tavaURL - + + Choose file... + + + + The token database will be saved at the following location: Märgistuste andmebaas salvestatakse järgmisesse asukohta: - + Save to a custom path (not recommended) Salvesta enda määratud asukohta (pole soovitatav) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Nukk-komplekt mis sisaldab märgistusi @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle sissetooja @@ -262,22 +292,22 @@ OutroPage - + Finished Valmis - + The wizard has finished. Võlur on lõpetanud. - + You can now start using Cockatrice with the newly updated cards. Nüüd saad Cockatrice’i kasutada uhiuute kaartidega. - + If the card databases don't reload automatically, restart the Cockatrice client. Kui kaardi andmebaasid ei lae ise, taaskäivita Cockatrice’i klient. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Viga - + No set has been imported. Komplekti pole sisse toodud. - + Sets imported Allalaetud komplektid - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: Leiti järgmised komplektid: - + Press "Save" to store the imported cards in the Cockatrice database. Vajuta „Salvesta“, et salvestada imporditud kaardid Cockatrice’i andmebaasi. - + The card database will be saved at the following location: Kaardi andmebaas salvestatakse järgmisesse asukohta: - + Save to a custom path (not recommended) Salvesta enda määratud asukohta (pole soovitatav) - + &Save &Salvesta - + Import finished: %1 cards. %1 kaarti imporditi edukalt. - + %1: %2 cards imported %1: imporditi %2 kaarti - + Save card database Salvesta kaartide andmebaas - + XML; card database (*.xml) XML; kaartide andmebaas (*.xml) - + The file could not be saved to %1 Faili salvestamine asukohta %1 ebaõnnestus @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Viga - + The provided URL is not valid: - + Downloading (0MB) Allalaadimine (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Allalaadimine (%1MB) - + Network error: %1. Võrgu viga: %1. - + The file could not be saved to %1 Faili salvestamine asukohta %1 nurjus @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + Zip-i tegevus oli edukas. Failed to initialize or load zlib library. - + Zlibi kogu ei suudetud ette valmistada või laadida. zlib library error. - + zlibi kogu viga. Unable to create or open file. - + Faili ei suudetud luua või avada. Partially corrupted archive. Some files might be extracted. - + Osaliselt vigane arhiiv. Ainult osa faile võidakse lahti pakkida. Corrupted archive. - + Kahjustunud arhiiv. Wrong password. - + Vale parool. No archive has been created yet. - + Loodud arhiivid puuduvad. File or directory does not exist. - + Faili või asukohta pole olemas. File read error. - + Faili lugemise viga. File write error. - + Faili kirjutamise viga. File seek error. - + Faili otsimise viga Unable to create a directory. - + Asukoha loomine ebaõnnestus. Invalid device. - + Vigane seade. Invalid or incompatible zip archive. - + Vigane või mittetoetatav zip-arhiiv. Inconsistent headers. Archive might be corrupted. - + Ebajäriekindlad päised. Arhiiv võib olla rikutud. Unknown error. - + Tundmatu viga. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + Zip-tegevus on valmis. Failed to initialize or load zlib library. - + Zlib kogu ei suudetud ette valmistada või laadida. zlib library error. - + zlibi kogu viga. Unable to create or open file. - + Faili loomine või avamine ebaõnnestus. No archive has been created yet. - + Loodud arhiivid puuduvad. File or directory does not exist. - + Faili või asukohta pole olemas. File read error. - + Faili lugemise viga. File write error. - + Faili kirjutamise viga. File seek error. - + Faili otsimise viga. Unknown error. - + Tundmatu viga. i18n - + English Eesti Keel (Estonian) diff --git a/oracle/translations/oracle_fi.ts b/oracle/translations/oracle_fi.ts index 093dec9ad..c7657cf7a 100644 --- a/oracle/translations/oracle_fi.ts +++ b/oracle/translations/oracle_fi.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Johdanto - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Tämä työkalu asentaa Cockatricessa käytettävät setit, kortit ja tokenit. - + Interface language: Käyttöliittymän kieli: - + Version: Versio: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Lähteen määritys - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Ole hyvä ja määritä lähde setti- ja korttilistalle. Voit määrittää ladattavan URL-osoitteen tai käyttää tietokoneellasi olevaa tiedostoa. - + Download URL: Ladattavan tiedoston URL-osoite: - + Local file: Paikallinen tiedosto: - + Restore default URL Palauta oletus-URL - + Choose file... Valitse tiedosto... - + Load sets file Ladatta ekspansiotiedosto - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Virhe - + The provided URL is not valid. Määritetty URL on virheellinen. - + Downloading (0MB) Ladataan (0MB) - + Please choose a file. Valitse tiedosto. - + Cannot open file '%1'. Ei voida avata tiedostoa '%1'. - + Downloading (%1MB) Ladataan (%1MB) - + Network error: %1. Yhteysvirhe: %1. - + Parsing file Jäsennellään tiedostoa - + Xz extraction failed. Xz purku epäonnistui. - + Sorry, this version of Oracle does not support xz compressed files. Pahoittelut. Tämä versio Oraclesta ei tue xz-pakattuja tiedostoja. - + Failed to open Zip archive: %1. Zip-tiedoston avaaminen epäonnistui: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Zip-tiedoston purkaminen epäonnistui: Zip-tiedosto ei sisällä tarkalleen yhtä tiedostoa. - + Zip extraction failed: %1. Zip-tiedoston purkaminen epäonnistuie: %1. - + Sorry, this version of Oracle does not support zipped files. Pahoittelut. Tämä versio Oraclesta ei tue Zip-pakattuja tiedostoja. - + Failed to interpret downloaded data. Ladattujen tietojen tulkitseminen epäonnistui. - + Do you want to download the uncompressed file instead? Haluatko sensijaan ladata pakkaamattoman tiedoston? - + The file was retrieved successfully, but it does not contain any sets data. Tiedosto palautettiin onnistuneesti, mutta se ei sisällä yhdenkään ekspansion dataa. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Tallenna spoileritietokanta - + XML; spoiler database (*.xml) XML; spoileritietokanta (*.xml) - + + spoiler + + + + Spoilers import Spoilerien lataus - + Please specify a compatible source for spoiler data. Ole hyvä ja osoita yhteensopiva spoileridatan lähde. - + Download URL: Ladattavan tiedoston URL-osoite: - + + Local file: + + + + Restore default URL Palauta oletus-URL - + + Choose file... + + + + The spoiler database will be saved at the following location: Spoileritietokanta tallennetaan seuraavaan sijaintiin: - + Save to a custom path (not recommended) Tallenna mukautettuun sijaintiin (ei-suositeltu) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Tallenna tokenitietokanta - + XML; token database (*.xml) XML; tokenitietokanta (*.xml) - + + tokens + + + + Tokens import Tokenien lataus - + Please specify a compatible source for token data. Ole hyvä ja osoita yhteensopiva tokenidatan lähde. - + Download URL: Ladattavan tiedoston URL-osoite: - + + Local file: + + + + Restore default URL Palauta oletus-URL - + + Choose file... + + + + The token database will be saved at the following location: Tokenitietokanta tallennetaan seuraavaan sijaintiin: - + Save to a custom path (not recommended) Tallenna mukautettuun sijaintiin (ei-suositeltu) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Tokeneja sisältävä mallisetti @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle-lataaja @@ -262,22 +292,22 @@ OutroPage - + Finished Valmis - + The wizard has finished. Asennusohjelma on valmis. - + You can now start using Cockatrice with the newly updated cards. Voit nyt käyttää Cockatricea juuri päivitetyillä korteilla. - + If the card databases don't reload automatically, restart the Cockatrice client. Jos korttitietokannat eivät päivity automaattisesti, käynnistä Cockatrice uudelleen. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Virhe - + No set has been imported. Yhtään settiä ei ladattu. - + Sets imported Ladatut ekspansiot - + A cockatrice database file of %1 MB has been downloaded. Cockatrice tietokantatiedosto kooltaan %1 MB on ladattu. - + The following sets have been found: Seuraavat setit löydettiin: - + Press "Save" to store the imported cards in the Cockatrice database. Paina "Tallenna" varastoidaksesi ladatut kortit Cockatricen tietokantaan. - + The card database will be saved at the following location: Korttitietokanta tallennetaan seuraavaan sijaintiin: - + Save to a custom path (not recommended) Tallenna mukautettuun sijaintiin (ei-suositeltu) - + &Save &Tallenna - + Import finished: %1 cards. Lataaminen valmis: %1 kortit. - + %1: %2 cards imported %1: %2 kortit ladattu - + Save card database Tallenna korttitietokanta - + XML; card database (*.xml) XML; korttitietokanta (*.xml) - + The file could not be saved to %1 Tiedostoa ei voitu tallentaa osoitteeseen %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Virhe - + The provided URL is not valid: - + Downloading (0MB) Ladataan (0 MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Ladataan (%1 MB) - + Network error: %1. Yhteysvirhe: %1. - + The file could not be saved to %1 Tiedostoa ei voitu tallentaa osoitteeseen %1 @@ -535,7 +587,7 @@ i18n - + English Suomi (Finnish) diff --git a/oracle/translations/oracle_fr.ts b/oracle/translations/oracle_fr.ts index b2beca4a4..47ab5d0fa 100644 --- a/oracle/translations/oracle_fr.ts +++ b/oracle/translations/oracle_fr.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Cet assistant va importer la liste des éditions, cartes et jetons qui seront utilisés par Cockatrice. - + Interface language: Langage de l'interface : - + Version: Version : @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Choix du fichier source - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Veuillez spécifier une source compatible pour la liste d'éditions et de cartes. Vous pouvez spécifier une URL qui sera utilisée pour télécharger un fichier ou utiliser un fichier existant sur votre ordinateur. - + Download URL: URL de téléchargement : - + Local file: Fichier local : - + Restore default URL Restaurer l'URL par défaut - + Choose file... Choisissez un fichier... - + Load sets file Charger une liste d'éditions - + Sets file (%1) Sets JSON file (%1) Choisir le fichier (%1) - - - - - - - + + + + + + + Error Erreur - + The provided URL is not valid. L'URL fournie n'est pas valable - + Downloading (0MB) Téléchargement en cours (0MB) - + Please choose a file. Choisissez un fichier. - + Cannot open file '%1'. Impossible d'ouvrir le fichier '%1'. - + Downloading (%1MB) Téléchargement (%1MB) - + Network error: %1. Erreur réseau : %1. - + Parsing file Traitement du fichier. - + Xz extraction failed. L'extraction du zip à échoué. - + Sorry, this version of Oracle does not support xz compressed files. Désolé, cette version d'Oracle ne supporte pas les fichiers zip. - + Failed to open Zip archive: %1. Impossible d'ouvrir l'archive zip: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Extraction zip échouée: l'archive zip contient plus qu'un fichier. - + Zip extraction failed: %1. L'extraction du zip a échoué : %1. - + Sorry, this version of Oracle does not support zipped files. Désolé, cette version d'Oracle ne supporte pas les fichiers zip. - + Failed to interpret downloaded data. Échec lors de l'interprétation des données téléchargées. - + Do you want to download the uncompressed file instead? Voulez-vous télécharger le fichier non compressé à la place ? - + The file was retrieved successfully, but it does not contain any sets data. Le fichier a été trouvé, mais ne contient aucune éditions. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Enregistrer la base de données des spoilers - + XML; spoiler database (*.xml) XML ; base de données des spoilers (*.xml) - + + spoiler + spoiler + + + Spoilers import Importation des spoilers - + Please specify a compatible source for spoiler data. Veuillez spécifier une source compatible pour les spoilers. - + Download URL: URL de téléchargement : - + + Local file: + Fichier local: + + + Restore default URL Restaurer l'URL par défaut - + + Choose file... + Choisissez un fichier... + + + The spoiler database will be saved at the following location: La base de données des spoilers sera enregistrée à l'emplacement suivant : - + Save to a custom path (not recommended) Sauvegarder à un emplacement personnalisé (non recommandé) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Enregistrer la base de données des jetons - + XML; token database (*.xml) XML ; base de données des jetons (*.xml) - + + tokens + jetons + + + Tokens import Importation des jetons - + Please specify a compatible source for token data. Veuillez spécifier une source compatible pour les jetons. - + Download URL: URL de téléchargement: - + + Local file: + Ficher local: + + + Restore default URL Restaurer l'URL par défaut - + + Choose file... + Choisissez un ficher... + + + The token database will be saved at the following location: La base de données des jetons sera enregistrée à l'emplacement suivant : - + Save to a custom path (not recommended) Sauvegarder à un emplacement personnalisé (non recommandé) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Fausse édition contenant les jetons @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Importateur Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished Terminé - + The wizard has finished. L'assistant a terminé. - + You can now start using Cockatrice with the newly updated cards. Vous pouvez maintenant commencer à utiliser Cockatrice avec les cartes mises à jour. - + If the card databases don't reload automatically, restart the Cockatrice client. Si les bases de données de cartes ne rechargent pas automatiquement, redémarrez le client Cockatrice. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Erreur - + No set has been imported. Aucune édition n'a été importé. - + Sets imported Éditions importées - + A cockatrice database file of %1 MB has been downloaded. Un fichier de base de données Cockatrice de %1 MB a été téléchargé. - + The following sets have been found: Les éditions suivantes ont été trouvées : - + Press "Save" to store the imported cards in the Cockatrice database. Cliquez sur « Enregistrer » pour enregistrer les cartes importées dans la base de données de Cockatrice. - + The card database will be saved at the following location: La base de données des cartes sera enregistrée à l'emplacement suivant : - + Save to a custom path (not recommended) Sauvegarder à un emplacement personnalisé (non recommandé) - + &Save Sauvegarder - + Import finished: %1 cards. Import terminé: %1 cartes. - + %1: %2 cards imported %1: %2 cartes ajoutées. - + Save card database Sauvegarder la base de carte - + XML; card database (*.xml) XML ; base de données de cartes (*.xml) - + The file could not be saved to %1 Le fichier n'a pu être sauvegarder au chemin '%1' @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + Chargez le fichier %1 + + + + %1 file (%1) + fichier %1 (%1) + + + + + + + Error Erreur - + The provided URL is not valid: L'URL fournie n'est pas valide : - + Downloading (0MB) Téléchargement (0 Mo) - + + Please choose a file. + Merci de choisir un fichier. + + + + Cannot open file '%1'. + Le fichier '%1' ne peut pas être ouvert. + + + Downloading (%1MB) Téléchargement (%1 Mo) - + Network error: %1. Erreur réseau : %1. - + The file could not be saved to %1 Le fichier n'a pas pu être enregistré dans %1 @@ -535,7 +587,7 @@ i18n - + English Français (French) diff --git a/oracle/translations/oracle_it.ts b/oracle/translations/oracle_it.ts index 192e8cc0e..34bcb3cbc 100644 --- a/oracle/translations/oracle_it.ts +++ b/oracle/translations/oracle_it.ts @@ -2,23 +2,23 @@ IntroPage - + Introduction Introduzione - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Questo wizard importerà la lista di set, carte e pedine che verranno usate da Cockatrice. - + Interface language: Lingua dell'interfaccia: - + Version: Versione: @@ -26,134 +26,134 @@ e pedine che verranno usate da Cockatrice. LoadSetsPage - + Source selection Selezione sorgente - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Specifica una sorgente compatibile per la lista dei set e delle carte. Puoi specificare un indirizzo URL da cui scaricare il file o selezionare un file già presente nel tuo computer. - + Download URL: Indirizzo download: - + Local file: File nel pc: - + Restore default URL Usa l'indirizzo predefinito - + Choose file... Scegli file... - + Load sets file Carica file dei set - + Sets file (%1) Sets JSON file (%1) File dei set (%1) - - - - - - - + + + + + + + Error Errore - + The provided URL is not valid. L'indirizzo specificato non è valido. - + Downloading (0MB) Scaricamento (0MB) - + Please choose a file. Seleziona un file. - + Cannot open file '%1'. Impossibile aprire il file '%1'. - + Downloading (%1MB) Scaricamento (%1MB) - + Network error: %1. Errore di rete: %1 - + Parsing file Analisi dei file - + Xz extraction failed. Estrazione da file xz fallita. - + Sorry, this version of Oracle does not support xz compressed files. Spiacente, ma questa versione di Oracle non supporta i file xz. - + Failed to open Zip archive: %1. Impossibile aprire il file Zip: %1 - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Estrazione file Zip fallita: lo Zip non contiene un solo file. - + Zip extraction failed: %1. Estrazione file Zip fallita: %1 - + Sorry, this version of Oracle does not support zipped files. Spiacente, ma questa versione di Oracle non supporta i file zip. - + Failed to interpret downloaded data. Impossibile interpretare i dati scaricati. - + Do you want to download the uncompressed file instead? Vuoi provare a scaricare il file non compresso? - + The file was retrieved successfully, but it does not contain any sets data. Il file è stato analizzato correttamente, ma non contiene i dati di nessun set. @@ -161,42 +161,57 @@ e pedine che verranno usate da Cockatrice. LoadSpoilersPage - + Save spoiler database Salva archivio spoiler - + XML; spoiler database (*.xml) XML; archivio spoiler (*.xml) - + + spoiler + spoiler + + + Spoilers import Importazione spoiler - + Please specify a compatible source for spoiler data. Specifica una sorgente compatibile per gli spoiler. - + Download URL: Indirizzo download: - + + Local file: + File nel pc: + + + Restore default URL Usa l'indirizzo predefinito - + + Choose file... + Scegli file... + + + The spoiler database will be saved at the following location: L'archivio degli spoiler verrà salvato nel seguente percorso: - + Save to a custom path (not recommended) Salva in un percorso diverso (sconsigliato) @@ -204,42 +219,57 @@ e pedine che verranno usate da Cockatrice. LoadTokensPage - + Save token database Salva archivio pedine - + XML; token database (*.xml) XML; archivio pedine (*.xml) - + + tokens + pedine + + + Tokens import Importazione pedine - + Please specify a compatible source for token data. Specifica una sorgente compatibile per le pedine. - + Download URL: Indirizzo download: - + + Local file: + File nel pc: + + + Restore default URL Usa l'indirizzo predefinito - + + Choose file... + Scegli file... + + + The token database will be saved at the following location: L'archivio delle pedine verrà salvato nel seguente percorso: - + Save to a custom path (not recommended) Salva in un percorso diverso (sconsigliato) @@ -247,7 +277,7 @@ e pedine che verranno usate da Cockatrice. OracleImporter - + Dummy set containing tokens Set finto contenente i token @@ -255,7 +285,7 @@ e pedine che verranno usate da Cockatrice. OracleWizard - + Oracle Importer Oracle Importer @@ -263,22 +293,22 @@ e pedine che verranno usate da Cockatrice. OutroPage - + Finished Finito - + The wizard has finished. Il wizard è completato. - + You can now start using Cockatrice with the newly updated cards. Adesso puoi iniziare ad usare Cockatrice con le nuove carte aggiornate. - + If the card databases don't reload automatically, restart the Cockatrice client. Se il database delle carte non si ricarica in automatico, riavvia il programma Cockatrice. @@ -286,73 +316,73 @@ e pedine che verranno usate da Cockatrice. SaveSetsPage - - + + Error Errore - + No set has been imported. Nessun set importato. - + Sets imported Set importati - + A cockatrice database file of %1 MB has been downloaded. È stato scaricato un file database di Cockatrice di %1 MB. - + The following sets have been found: Sono stati trovati i seguenti set: - + Press "Save" to store the imported cards in the Cockatrice database. Premi "Salva" per salvare le carte importate nel database di Cockatrice. - + The card database will be saved at the following location: L'archivio delle carte verrà salvato nel seguente percorso: - + Save to a custom path (not recommended) Salva in un percorso diverso (sconsigliato) - + &Save &Salva - + Import finished: %1 cards. Importazione conclusa: %1 carte. - + %1: %2 cards imported %1: %2 carte importate - + Save card database Salva archivio carte - + XML; card database (*.xml) XML; archivio carte (*.xml) - + The file could not be saved to %1 Impossibile salvare il file su %1 @@ -360,34 +390,56 @@ e pedine che verranno usate da Cockatrice. SimpleDownloadFilePage - - - + + Load %1 file + Carica %1 file + + + + %1 file (%1) + %1 file (%1) + + + + + + + Error Errore - + The provided URL is not valid: L'indirizzo fornito non è valido: - + Downloading (0MB) Scaricamento (0MB) - + + Please choose a file. + Seleziona un file. + + + + Cannot open file '%1'. + Impossibile aprire il file '%1'. + + + Downloading (%1MB) Scaricamento (%1MB) - + Network error: %1. Errore di rete: %1. - + The file could not be saved to %1 Impossibile salvare il file su %1 @@ -536,7 +588,7 @@ e pedine che verranno usate da Cockatrice. i18n - + English Italiano (Italian) diff --git a/oracle/translations/oracle_ja.ts b/oracle/translations/oracle_ja.ts index 1900188e8..8319f37a2 100644 --- a/oracle/translations/oracle_ja.ts +++ b/oracle/translations/oracle_ja.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction はじめに - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. このウィザードでは、Cockatriceで使用されるカードやトークン、セットのリストをインポートします。 - + Interface language: インターフェース言語: - + Version: バージョン: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection ソース選択 - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. セットとカードのリストの互換性のあるソースを指定してください。ダウンロードするURLアドレスを指定するか、コンピューターから既存のファイルを使用できます。 - + Download URL: ダウンロードURL: - + Local file: ローカルファイル: - + Restore default URL デフォルトのURLを復元 - + Choose file... ファイルを選択... - + Load sets file カードセットファイルを開く - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error エラー - + The provided URL is not valid. 指定されたURLは無効です。 - + Downloading (0MB) ダウンロード中 (0MB) - + Please choose a file. ファイルを選択してください。 - + Cannot open file '%1'. '%1'を開けませんでした。 - + Downloading (%1MB) ダウンロード中 (%1MB) - + Network error: %1. ネットワークエラー: %1。 - + Parsing file ファイルの解析 - + Xz extraction failed. Xz展開に失敗。 - + Sorry, this version of Oracle does not support xz compressed files. このバージョンのOracleはxz圧縮ファイルをサポートしていません。 - + Failed to open Zip archive: %1. ZIPアーカイブの展開に失敗: %1。 - + Zip extraction failed: the Zip archive doesn't contain exactly one file. ZIP展開に失敗:Zipアーカイブに含まれるファイルが1つだけではありません。 - + Zip extraction failed: %1. ZIP展開に失敗: %1。 - + Sorry, this version of Oracle does not support zipped files. 申し訳ありませんが現バージョンのOracleはzip形式のファイルをサポートしていません。 - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? 代わりに非圧縮ファイルをダウンロードしますか? - + The file was retrieved successfully, but it does not contain any sets data. ファイルは正常に取得されましたが、カードセットのデータが含まれていませんでした。 @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database スポイラーデータベースを保存 - + XML; spoiler database (*.xml) XML; スポイラーデータベース (*.xml) - + + spoiler + + + + Spoilers import スポイラーインポート - + Please specify a compatible source for spoiler data. 互換性のあるスポイラーデータのソースを指定してください。 - + Download URL: ダウンロードURL: - + + Local file: + + + + Restore default URL デフォルトのURLを復元 - + + Choose file... + + + + The spoiler database will be saved at the following location: スポイラーデータベースは、以下の場所に保存されます: - + Save to a custom path (not recommended) 別のパスに保存(非推奨) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database トークンデータベースを保存 - + XML; token database (*.xml) XML; トークンデータベース (*.xml) - + + tokens + + + + Tokens import トークンのインポート - + Please specify a compatible source for token data. トークンデータのソースを選択してください。 - + Download URL: ダウンロードURL: - + + Local file: + + + + Restore default URL デフォルトのURLを復元 - + + Choose file... + + + + The token database will be saved at the following location: トークンデータベースは以下の場所に保存されます。 - + Save to a custom path (not recommended) 別のパスに保存(非推奨) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens ダミーセットを含むトークン @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle Importer - オラクル・インポーター @@ -262,22 +292,22 @@ OutroPage - + Finished 完了しました! - + The wizard has finished. ウィザードが完了しました。 - + You can now start using Cockatrice with the newly updated cards. Cockatriceで新しく更新されたカードを使うことが出来ます。 - + If the card databases don't reload automatically, restart the Cockatrice client. カードデータベースが自動的に再読込されない場合は、Cockatriceを再起動して下さい。 @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error エラー - + No set has been imported. セットはインポートされませんでした。 - + Sets imported カードセットインポート - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: 次のセットが見つかりました: - + Press "Save" to store the imported cards in the Cockatrice database. ”保存”をクリックするとインポートしたカードをCockatriceデータベースに保存します。 - + The card database will be saved at the following location: カードデータベースは以下の場所に保存されます: - + Save to a custom path (not recommended) 別のパスに保存(非推奨) - + &Save 保存 - + Import finished: %1 cards. %1枚のカードがインポートされました。 - + %1: %2 cards imported %1: %2枚のカードがインポートされました。 - + Save card database カードデータベースを保存 - + XML; card database (*.xml) XML; card database (*.xml) - + The file could not be saved to %1 %1に保存できませんでした。 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error エラー - + The provided URL is not valid: - + Downloading (0MB) ダウンロード中 (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) ダウンロード中 (%1MB) - + Network error: %1. ネットワークエラー: %1。 - + The file could not be saved to %1 %1に保存できませんでした。 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + 正常に完了しました。 Failed to initialize or load zlib library. - + 初期化またはzlibライブラリのロードに失敗しました。 zlib library error. - + zlibライブラリエラー。 Unable to create or open file. - + ファイルが作成または開けませんでした。 Partially corrupted archive. Some files might be extracted. - + アーカイブが部分的に破損しています。いくつかのファイルが抽出されることがあります。 Corrupted archive. - + 破損したアーカイブ。 Wrong password. - + パスワードが間違っています。 No archive has been created yet. - + アーカイブは作成されませんでした。 File or directory does not exist. - + ファイルまたはフォルダが存在しません。 File read error. - + ファイル読み込みエラー。 File write error. - + ファイル書き込みエラー。 File seek error. - + シーク エラー。 Unable to create a directory. - + フォルダが作成できませんでした。 Invalid device. - + 無効なデバイス。 Invalid or incompatible zip archive. - + 無効なまたは互換性のないzipアーカイブ。 Inconsistent headers. Archive might be corrupted. - + 一貫性のないヘッダー。アーカイブが壊れている可能性があります。 Unknown error. - + 不明なエラー。 @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + 正常に完了しました。 Failed to initialize or load zlib library. - + 初期化またはzlibライブラリのロードに失敗しました。 zlib library error. - + zlibライブラリエラー。 Unable to create or open file. - + ファイルが作成または開けませんでした。 No archive has been created yet. - + アーカイブは作成されませんでした。 File or directory does not exist. - + ファイルまたはフォルダが存在しません。 File read error. - + ファイル読み込みエラー。 File write error. - + ファイル書き込みエラー。 File seek error. - + シーク エラー。 Unknown error. - + 不明なエラー。 i18n - + English 日本語 (Japanese) diff --git a/oracle/translations/oracle_ko.ts b/oracle/translations/oracle_ko.ts index 215eaa19e..c5c24583f 100644 --- a/oracle/translations/oracle_ko.ts +++ b/oracle/translations/oracle_ko.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction 개요 - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection 확장판 목록 파일 주소 입력 - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: 다운로드 주소: - + Local file: 파일 위치: - + Restore default URL 기본 주소로 복원 - + Choose file... 파일 선택... - + Load sets file 확장판 목록 파일 불러오기 - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error 오류 - + The provided URL is not valid. 잘못된 주소를 입력하셨습니다. - + Downloading (0MB) 다운로드 중 (0MB) - + Please choose a file. 확장판 목록 파일을 선택해 주세요. - + Cannot open file '%1'. 파일 '%1'을(를) 열 수 없습니다. - + Downloading (%1MB) 다운로드 중 (%1MB) - + Network error: %1. 네트워크 오류 : %1. - + Parsing file 목록 파싱중 - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. 압축파일 열기 실패 : %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. 압축 풀기 실패 : 압축 파일에 확장판 목록 파일 이외의 파일이 있습니다. - + Zip extraction failed: %1. 압축 풀기 실패 : %1. - + Sorry, this version of Oracle does not support zipped files. 죄송합니다. 본 버전에서는 압축 파일을 지원하지 않습니다. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. 파일을 성공적으로 다운로드 하였으나 확장판 정보가 들어있지 않습니다. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: 웹 주소: - + + Local file: + + + + Restore default URL 기본 주소로 복원 - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens 토큰 정보가 들어있는 더미 확장판 @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer 오라클 @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error 오류 - + No set has been imported. 아무 확장판도 불러오지 못했습니다. - + Sets imported 확장판 불러오기 완료 - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. 총 %1장의 카드 불러오기 완료 - + %1: %2 cards imported %1에서 %2장의 카드 불러옴 - + Save card database 카드 데이터베이스 저장 - + XML; card database (*.xml) 카드 데이터베이스 XML 파일 (*.xml) - + The file could not be saved to %1 파일을 %1에 저장 할 수 없습니다. @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + 압축파일 작업을 성공적으로 완료하였습니다. Failed to initialize or load zlib library. - + zlib 라이브러리를 초기화하거나 불러올 수 없습니다. zlib library error. - + zlib 라이브러리 오류. Unable to create or open file. - + 파일을 만들거나 열 수 없습니다. Partially corrupted archive. Some files might be extracted. - + 압축파일의 일부가 손상되었습니다. 몇몇 파일은 압축이 풀렸을 수도 있습니다. Corrupted archive. - + 압축파일이 손상되었습니다. Wrong password. - + 비밀번호가 틀렸습니다. No archive has been created yet. - + 압축파일이 아직 생성되지 않았습니다. File or directory does not exist. - + 파일이나 디렉토리가 존재하지 않습니다. File read error. - + 파일 읽기 오류. File write error. - + 파일 쓰기 오류. File seek error. - + 파일 찾기 오류. Unable to create a directory. - + 디렉토리를 생성할 수 없었습니다. Invalid device. - + 잘못된 장치입니다. Invalid or incompatible zip archive. - + 잘못되거나 지원하지 않는 압축파일입니다. Inconsistent headers. Archive might be corrupted. - + 헤더가 손상되었습니다. 압축 파일이 손상됐을 가능성이 있습니다. Unknown error. - + 알 수 없는 오류. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + 압축파일 작업을 성공적으로 완료하였습니다. Failed to initialize or load zlib library. - + zlib 라이브러리를 초기화하거나 불러올 수 없습니다. zlib library error. - + zlib 라이브러리 오류. Unable to create or open file. - + 파일을 만들거나 열 수 없습니다. No archive has been created yet. - + 압축파일이 아직 생성되지 않았습니다. File or directory does not exist. - + 파일이나 디렉토리가 존재하지 않습니다. File read error. - + 파일 읽기 오류. File write error. - + 파일 쓰기 오류. File seek error. - + 파일 찾기 오류. Unknown error. - + 알 수 없는 오류. i18n - + English 한국어 (Korean) diff --git a/oracle/translations/oracle_nb.ts b/oracle/translations/oracle_nb.ts index 856cffdbf..b40868cc1 100644 --- a/oracle/translations/oracle_nb.ts +++ b/oracle/translations/oracle_nb.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introduksjon - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Kilde valg - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: Nedlastings URL - + Local file: Lokal fil - + Restore default URL Gjenopprett standard URL - + Choose file... Velg fil... - + Load sets file Last set fil... - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Feil - + The provided URL is not valid. URL du anga er ikke gyldig. - + Downloading (0MB) Laster ned (0MB) - + Please choose a file. Vennligst velg en fil - + Cannot open file '%1'. Kunne ikke åpme '%1' - + Downloading (%1MB) Laster ned (%1MB) - + Network error: %1. Nettverks feil: %1 - + Parsing file Tolker fil. - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. Kunne ikke åpne Zip Arkiv: %1 - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Zip ekstraksjon feilet: Zip arkivet inneholder ikke nøyaktig en fil - + Zip extraction failed: %1. Zip ekstraksjon feilet: %1 - + Sorry, this version of Oracle does not support zipped files. Beklager, denne versjonen av Oracle støtter ikke Zip filer. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. Filen ble hentet riktig, men den inneholder ikke noe set data @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: Nedlastings URL - + + Local file: + + + + Restore default URL Gjenopprett standard URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Dummy sett som inneholder tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle importerer @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Feil - + No set has been imported. Ingen set har blitt importert. - + Sets imported Set importert - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. Importering fullført: %1 kort. - + %1: %2 cards imported %1: %2 kort importert. - + Save card database Lagre kort database. - + XML; card database (*.xml) XML; kort database (*.xml) - + The file could not be saved to %1 Filen kunne ikke lagres til %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + Zip operasjonen fullført. Failed to initialize or load zlib library. - + Kunne ikke initialisere eller laste zlib mappen. zlib library error. - + Feil med zlib mappen Unable to create or open file. - + Kunne ikke lage eller åpne fil. Partially corrupted archive. Some files might be extracted. - + Delvis korrupt arkiv. Noen filer kunne ikke bli ekstraktert. Corrupted archive. - + Korrupt arkiv. Wrong password. - + Feil passord No archive has been created yet. - + Det har ikke blitt laget noe arkiv enda. File or directory does not exist. - + Fil eller katalog eksisterer ikke. File read error. - + Kunne ikke lese filen. File write error. - + Kunne ikke skrive fil. File seek error. - + Kunne ikke finne filen. Unable to create a directory. - + Kunne ikke lage katalog. Invalid device. - + Ugyldig apparat. Invalid or incompatible zip archive. - + Ugyldig eller inkompatibelt zip akriv. Inconsistent headers. Archive might be corrupted. - + Ukonsistente overskrifter. Arkivet kan være korrupt. Unknown error. - + Ukjent feil. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + Zip operasjonen fullført. Failed to initialize or load zlib library. - + Kunne ikke initialisere eller laste zlib biblioteket. zlib library error. - + Feil med zlib mappen Unable to create or open file. - + Kunne ikke lage eller åpne fil. No archive has been created yet. - + Det har ikke blitt laget noe arkiv enda. File or directory does not exist. - + Fil eller katalog eksisterer ikke. File read error. - + Kunne ikke lese filen. File write error. - + Kunne ikke skrive fil. File seek error. - + Kunne ikke finne filen. Unknown error. - + Ukjent feil. i18n - + English Norsk Bokmål (Norwegian Bokmål) diff --git a/oracle/translations/oracle_nl.ts b/oracle/translations/oracle_nl.ts index 606a3f324..ed9a5fae9 100644 --- a/oracle/translations/oracle_nl.ts +++ b/oracle/translations/oracle_nl.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introductie - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Deze wizard importeert de lijst met sets, kaarten en tokens die door Cockatrice zullen worden gebruikt. - + Interface language: Interface taal: - + Version: Versie: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Bron selectie - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Gelieve een compatibele bron te vermelden voor de lijst van sets en kaarten. U kunt een URL-adres opgeven dat wordt gedownload of een bestaand bestand van uw computer gebruiken. - + Download URL: Download URL: - + Local file: Lokaal bestand - + Restore default URL Herstel standaard URL - + Choose file... Bestand kiezen... - + Load sets file Bestand laden - + Sets file (%1) Sets JSON file (%1) - + Sets bestand (%1) - - - - - - - + + + + + + + Error Fout - + The provided URL is not valid. De ingevoerde URL is niet geldig. - + Downloading (0MB) Downloaden (0MB) - + Please choose a file. Gelieve een bestand te kiezen. - + Cannot open file '%1'. Bestand '%1' kan niet geopend worden. - + Downloading (%1MB) Downloaden (%1MB) - + Network error: %1. Netwerk fout: %1. - + Parsing file Parsen van bestand - + Xz extraction failed. Xz extractie mislukt. - + Sorry, this version of Oracle does not support xz compressed files. Sorry, deze versie van Oracle ondersteunt geen xz gecomprimeerde bestanden. - + Failed to open Zip archive: %1. Zip archief kan niet geopend worden: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Uitpakken van Zip niet gelukt: het archief moet precies één bestand bevatten. - + Zip extraction failed: %1. Uitpakken van Zip niet gelukt: %1. - + Sorry, this version of Oracle does not support zipped files. Sorry, deze versie van Oracle ondersteunt geen gecomprimeerde bestanden. - + Failed to interpret downloaded data. Lezen van gedownloade data mislukt. - + Do you want to download the uncompressed file instead? Wilt u het ongecomprimeerde bestand downloaden? - + The file was retrieved successfully, but it does not contain any sets data. Het bestand is succesvol binnengehaald, maar bevat geen set data. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Opslaan spoiler database - + XML; spoiler database (*.xml) XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import Spoilers import - + Please specify a compatible source for spoiler data. Gelieve een compatibele bron voor spoilergegevens te specificeren. - + Download URL: Download URL: - + + Local file: + + + + Restore default URL Herstel standaard URL - + + Choose file... + + + + The spoiler database will be saved at the following location: De spoiler database wordt op de volgende locatie opgeslagen: - + Save to a custom path (not recommended) Opslaan naar een aangepaste locatie (niet aanbevolen) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database token database opslaan - + XML; token database (*.xml) XML; token database (*.xml) - + + tokens + + + + Tokens import Tokens import - + Please specify a compatible source for token data. Gelieve een compatibele bron voor tokengegevens op te geven. - + Download URL: Download URL: - + + Local file: + + + + Restore default URL Herstel standaard URL - + + Choose file... + + + + The token database will be saved at the following location: De token database wordt op de volgende locatie opgeslagen: - + Save to a custom path (not recommended) Opslaan naar een aangepaste locatie (niet aanbevolen) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Token voorbeeldset @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle importer @@ -262,22 +292,22 @@ OutroPage - + Finished Klaar - + The wizard has finished. De wizard is klaar. - + You can now start using Cockatrice with the newly updated cards. U kunt nu beginnen met het gebruik van Cockatrice met de nieuw bijgewerkte kaarten. - + If the card databases don't reload automatically, restart the Cockatrice client. Als de kaartendatabases niet automatisch herladen, start dan de Cockatrice client opnieuw op. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Fout - + No set has been imported. Er zijn geen sets geïmporteerd. - + Sets imported Sets geïmporteerd - + A cockatrice database file of %1 MB has been downloaded. Een cockatrice database bestand van %1 MB is gedownload. - + The following sets have been found: De volgende sets zijn gevonden: - + Press "Save" to store the imported cards in the Cockatrice database. Druk op "Opslaan" om de geïmporteerde kaarten op te slaan in de Cockatrice database. - + The card database will be saved at the following location: De kaartendatabase wordt op de volgende locatie opgeslagen: - + Save to a custom path (not recommended) Opslaan naar een aangepaste locatie (niet aanbevolen) - + &Save &Opslaan - + Import finished: %1 cards. Import klaar: %1 kaarten. - + %1: %2 cards imported %1: %2 kaarten geïmporteerd - + Save card database Kaartendatabase opslaan - + XML; card database (*.xml) XML; kaart database (*.xml) - + The file could not be saved to %1 Het bestand kon niet worden opgeslagen in %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Fout - + The provided URL is not valid: - + De ingevoerde URL is niet geldig: - + Downloading (0MB) Downloaden (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Downloaden (%1MB) - + Network error: %1. Netwerk fout: %1. - + The file could not be saved to %1 Het bestand kon niet worden opgeslagen naar %1 @@ -535,7 +587,7 @@ i18n - + English Nederlands (Dutch) @@ -550,7 +602,7 @@ Run in no-confirm background mode - + Uitvoeren in achtergrondmodus zonder bevestiging \ No newline at end of file diff --git a/oracle/translations/oracle_pl.ts b/oracle/translations/oracle_pl.ts index 6ccdfdd47..d5d632f70 100644 --- a/oracle/translations/oracle_pl.ts +++ b/oracle/translations/oracle_pl.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Wprowadzenie - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Ten kreator zaimportuje listę dodatków, kart oraz tokenów które zostaną użyte przez Cockatrice. - + Interface language: Język interfejsu: - + Version: Wersja: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Wybór źródła - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Proszę podać źródło listy edycji i kart. Można podać adres URL, z którego zostanie pobrana, lub istniejący plik na komputerze. - + Download URL: Pobierz URL - + Local file: Plik lokalny: - + Restore default URL Przywróć domyślny URL - + Choose file... Wybierz plik… - + Load sets file Wczytaj listę dodatków - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Błąd - + The provided URL is not valid. Podano nieprawidłowy URL. - + Downloading (0MB) Pobieranie (0MB) - + Please choose a file. Proszę wybrać plik. - + Cannot open file '%1'. Nie można otworzyć pliku '%1'. - + Downloading (%1MB) Pobieranie (%1MB) - + Network error: %1. Błąd sieci: %1. - + Parsing file Analizowanie pliku - + Xz extraction failed. Rozpakowanie XZ nie udało się. - + Sorry, this version of Oracle does not support xz compressed files. Przepraszamy, ta wersja Oracle nie obsługuje plików spakowanych w archiwum xz. - + Failed to open Zip archive: %1. Otwieranie archiwum Zip zakończone niepowodzeniem: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Rozpakowanie pliku Zip nieudane: archiwum nie zawiera dokładnie jednego pliku. - + Zip extraction failed: %1. Rozpakowanie Zip nieudane: %1. - + Sorry, this version of Oracle does not support zipped files. Przepraszamy, ta wersja Oracle nie obsługuje plików spakowanych w archiwum Zip. - + Failed to interpret downloaded data. Interpretacja pobranych danych nie udała się. - + Do you want to download the uncompressed file instead? Czy chcesz zamiast tego pobrać plik nieskompresowany? - + The file was retrieved successfully, but it does not contain any sets data. Plik został pobrany z powodzeniem, ale nie zawiera informacji o dodatkach. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Zapisz bazę danych spoilerów. - + XML; spoiler database (*.xml) XML; baza danych spoilerów (*.xml) - + + spoiler + + + + Spoilers import Import spoilerów - + Please specify a compatible source for spoiler data. Proszę wybrać kompatybilne źródło danych ze spoilerami. - + Download URL: URL pobierania: - + + Local file: + + + + Restore default URL Przywróć domyślny URL - + + Choose file... + + + + The spoiler database will be saved at the following location: Baza danych spoilerów zostanie zapisana w tym miejscu: - + Save to a custom path (not recommended) Zapisz do niestandardowej ścieżki (niezalecane) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Zapisz bazę danych tokenów. - + XML; token database (*.xml) XML; baza danych tokenów (*.xml) - + + tokens + + + + Tokens import Import tokenów - + Please specify a compatible source for token data. Proszę wybrać kompatybilne źródło danych z tokenami. - + Download URL: Pobierz odnośnik: - + + Local file: + + + + Restore default URL Przywróć domyślny URL - + + Choose file... + + + + The token database will be saved at the following location: Baza danych tokenów zostanie zapisana w tym miejscu: - + Save to a custom path (not recommended) Zapisz do niestandardowej ścieżki (niezalecane) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Dodatek-atrapa, zawierający tokeny. @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle – kreator importu @@ -262,22 +292,22 @@ OutroPage - + Finished Zakończone - + The wizard has finished. Kreator zakończył swoją pracę. - + You can now start using Cockatrice with the newly updated cards. Możesz teraz korzystać z Cockatrice z nowo zaktualizowanymi kartami. - + If the card databases don't reload automatically, restart the Cockatrice client. Jeżeli bazy kart nie przeładują się automatycznie, zrestartuj klienta Cockatrice. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Błąd - + No set has been imported. Nie zaimportowano żadnego dodatku. - + Sets imported Zaimportowane dodatki - + A cockatrice database file of %1 MB has been downloaded. Baza danych Cockatrice mająca %1 MB została pobrana. - + The following sets have been found: Następujące dodatki zostały znalezione: - + Press "Save" to store the imported cards in the Cockatrice database. Kliknij "Zapisz" aby zapisać zaimportowane karty w bazie danych Cockatrice. - + The card database will be saved at the following location: Baza danych kart zostanie zapisana w tym miejscu: - + Save to a custom path (not recommended) Zapisz do niestandardowej ścieżki (niezalecane) - + &Save Zapisz - + Import finished: %1 cards. Import zakończony: %1 kart. - + %1: %2 cards imported %1: zaimportowano %2 kart - + Save card database Zapisz bazę kart - + XML; card database (*.xml) XML, baza kart (*.xml) - + The file could not be saved to %1 Plik nie mógł zostać zapisany do %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Błąd - + The provided URL is not valid: - + Downloading (0MB) Pobieranie (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Pobieranie (%1MB) - + Network error: %1. Błąd sieci: %1. - + The file could not be saved to %1 Plik nie mógł zostać zapisany do %1 @@ -535,7 +587,7 @@ i18n - + English Polski (Polish) diff --git a/oracle/translations/oracle_pt.ts b/oracle/translations/oracle_pt.ts index 3f3422a8f..c578a0365 100644 --- a/oracle/translations/oracle_pt.ts +++ b/oracle/translations/oracle_pt.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introdução - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Selecção da fonte - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: URL de download: - + Local file: Ficheiro local: - + Restore default URL URL para repor definições de origem - + Choose file... Escolher ficheiro... - + Load sets file Carregar ficheiro das edições - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Erro - + The provided URL is not valid. O URL fornecido não é válido. - + Downloading (0MB) A efectuar download (0MB) - + Please choose a file. Por favor escolha um ficheiro. - + Cannot open file '%1'. Impossível abrir ficheiro '%1'. - + Downloading (%1MB) A efectuar download (%1MB) - + Network error: %1. Erro da rede: %1. - + Parsing file Ficheiro de análise - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. Abrir archivo zip falhou: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Extracção do ZIP falhada: o arquivo ZIP não contem exactamente um ficheiro. - + Zip extraction failed: %1. Extração do Zip falhada: %1. - + Sorry, this version of Oracle does not support zipped files. Pedimos desculpa, mas esta versão do Oracle não suporta ficheiros zipados. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. O ficheiro foi recuperado com sucesso, mas não contem nenhum dado sobre expansões. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: URL de download: - + + Local file: + + + + Restore default URL URL para repor definições de origem - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Set básico contendo fichas @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Importar Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Erro - + No set has been imported. Nenhuma expansão foi importada. - + Sets imported Edições importadas - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. Importação terminada: %1 cartas. - + %1: %2 cards imported %1. %2 cartas importadas - + Save card database Guardar base de dados das cartas - + XML; card database (*.xml) XML; Base de dados de cartas (*.xml) - + The file could not be saved to %1 O ficheiro não pode ser gravado em %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + Operação ZIP completa com sucesso. Failed to initialize or load zlib library. - + Falha na inicialização ou carregamento da biblioteca zlib zlib library error. - + Erro da biblioteca zlib Unable to create or open file. - + Incapaz de criar ou abrir ficheiro Partially corrupted archive. Some files might be extracted. - + Arquivo parcialmente corrompido. Alguns ficheiros possivelmente serão extraídos. Corrupted archive. - + Archivo corrompido. Wrong password. - + Senha incorrecta. No archive has been created yet. - + Ainda não foi criado nenhum arquivo File or directory does not exist. - + O ficheiro ou directório não existe. File read error. - + Erro na leitura do ficheiro. File write error. - + erro na escrita do ficheiro. File seek error. - + Erro na procura do ficheiro. Unable to create a directory. - + Incapaz de criar directório. Invalid device. - + Dispositivo inválido. Invalid or incompatible zip archive. - + Arquivo ZIP inválido ou incompatível Inconsistent headers. Archive might be corrupted. - + Cabeçalhos inconsistentes. O arquivo pode estar corrompido, Unknown error. - + Erro desconhecido. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + Operação ZIP completa com sucesso. Failed to initialize or load zlib library. - + Falha na inicialização ou carregamento da biblioteca zlib zlib library error. - + Erro da biblioteca zlib Unable to create or open file. - + Incapaz de criar ou abrir ficheiro No archive has been created yet. - + Ainda não foi criado nenhum arquivo File or directory does not exist. - + O ficheiro ou directório não existe. File read error. - + Erro na leitura do ficheiro. File write error. - + erro na escrita do ficheiro. File seek error. - + Erro na procura do ficheiro. Unknown error. - + Erro desconhecido. i18n - + English Português (Portuguese) diff --git a/oracle/translations/oracle_pt_BR.ts b/oracle/translations/oracle_pt_BR.ts index 566f018c9..96a6382ec 100644 --- a/oracle/translations/oracle_pt_BR.ts +++ b/oracle/translations/oracle_pt_BR.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Introdução - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Esse recurso irá importar a lista de coleções, cartas e fichas que serão utilizadas pelo Cokatrice. - + Interface language: Idioma da interface: - + Version: Versão: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Selecione a origem - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Por favor especifique uma fonte compatível de expansões e cartas. Você pode especificar um endereço URL que será baixado ou usar um arquivo existente no seu computador. - + Download URL: Endereço para download: - + Local file: Arquivo local: - + Restore default URL Restaurar URL padrão - + Choose file... Escolha o arquivo... - + Load sets file Carregar arquivos de expansão - + Sets file (%1) Sets JSON file (%1) Arquivo de expansões (%1) - - - - - - - + + + + + + + Error Erro - + The provided URL is not valid. A URL escolhida não é valida - + Downloading (0MB) Baixando (0MB) - + Please choose a file. Por favor, escolha um arquivo; - + Cannot open file '%1'. Não pode abrir arquivo '%1' - + Downloading (%1MB) Baixando (%1MB) - + Network error: %1. Erro na conexão com a internet: %1. - + Parsing file Analisando arquivo - + Xz extraction failed. Extração de arquivo .xz falhou - + Sorry, this version of Oracle does not support xz compressed files. Desculpe, esta versão do Oracle não suporta arquivos comprimidos no formato .xz - + Failed to open Zip archive: %1. Falhou em abrir o arquivo Zip: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. A extração do arquivo ZIP falhou: o arquivo ZIP não contém exatamente um arquivo. - + Zip extraction failed: %1. Extração do arquivo Zip falhou: %1. - + Sorry, this version of Oracle does not support zipped files. Desculpe, esta versão do Oracle não suporta arquivos compactados. - + Failed to interpret downloaded data. Falha ao interpretar dados baixados - + Do you want to download the uncompressed file instead? Gostaria de baixar os arquivos descomprimidos, em vez disso? - + The file was retrieved successfully, but it does not contain any sets data. O arquivo foi recebido com sucesso, mas não contém qualquer informação de expansão. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Salvar base de dados de spoiler - + XML; spoiler database (*.xml) XML; base de dados de spoilers (*.xml) - + + spoiler + + + + Spoilers import Importar spoilers - + Please specify a compatible source for spoiler data. Por favor, selecione uma fonte compatível para os dados de spoilers. - + Download URL: Endereço para download: - + + Local file: + + + + Restore default URL Restaurar URL padrão - + + Choose file... + + + + The spoiler database will be saved at the following location: A base de dados de spoilers será salva no seguinte diretório: - + Save to a custom path (not recommended) Salvar em diretório customizado (não recomendado) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database Salvar base de dados de tokens - + XML; token database (*.xml) XML; base de dados de token (*.xml) - + + tokens + + + + Tokens import Importar tokens - + Please specify a compatible source for token data. Por favor especifique uma fonte compatível para os dados de tokens. - + Download URL: Endereço para download: - + + Local file: + + + + Restore default URL Restaurar URL padrão - + + Choose file... + + + + The token database will be saved at the following location: A base de dados de tokens será salva no seguinte diretório: - + Save to a custom path (not recommended) Salvar em diretório customizado (não recomendado) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Esta expansão contém fichas. @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Importador Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished Finalizado - + The wizard has finished. O recurso terminou. - + You can now start using Cockatrice with the newly updated cards. Agora você pode começar a usar o Cockatrice com as novas cartas atualizadas. - + If the card databases don't reload automatically, restart the Cockatrice client. Se a base de dados das cartas não for atualizada automaticamente, reinicie o cliente do Cockatrice. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Erro - + No set has been imported. O set não foi importado. - + Sets imported Expansões importadas - + A cockatrice database file of %1 MB has been downloaded. Um arquivo de %1 MB de banco de dados do cockatrice foi baixado. - + The following sets have been found: As seguintes expansões foram encontradas: - + Press "Save" to store the imported cards in the Cockatrice database. Aperte "Salvar" para guardar as cartas importadas na base de dados do Cockatrice. - + The card database will be saved at the following location: A base de dados de cartas será salva no seguinte diretório: - + Save to a custom path (not recommended) Salvar em diretório customizado (não recomendado) - + &Save &Salvar - + Import finished: %1 cards. Importação finalizada: %1 cartas. - + %1: %2 cards imported %1: %2 cartas importadas - + Save card database Carta salva no banco de dados - + XML; card database (*.xml) XML; banco de dados de cartas (*.xml) - + The file could not be saved to %1 O arquivo não pode ser salvo para %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error Erro - + The provided URL is not valid: - + Downloading (0MB) Baixando (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) Baixando (%1MB) - + Network error: %1. Erro de rede: %1 - + The file could not be saved to %1 O arquivo não pode ser salvo em %1 @@ -535,7 +587,7 @@ i18n - + English Português do Brasil (Brazilian Portuguese) diff --git a/oracle/translations/oracle_ru.ts b/oracle/translations/oracle_ru.ts index 5eae49b17..e2ff5b5a0 100644 --- a/oracle/translations/oracle_ru.ts +++ b/oracle/translations/oracle_ru.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Введение - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Эта программа импортирует перечень выпусков, карт и фишек, которые будут использоваться в Cockatrice. - + Interface language: - + Язык интерфейса: - + Version: Версия @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Выбор источника - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Пожалуйста, укажите источник для перечня сетов и карт. Вы можете использовать ссылку или уже существующий локальный файл. - + Download URL: Ссылка на скачивание: - + Local file: Локальный файл: - + Restore default URL Восстановить ссылку по умолчанию - + Choose file... Выбрать файл.. - + Load sets file Загрузить файл сетов - + Sets file (%1) Sets JSON file (%1) - + Файл (%1) - - - - - - - + + + + + + + Error Ошибка - + The provided URL is not valid. Предоставленная ссылка некорректна. - + Downloading (0MB) Загрузка (0MB) - + Please choose a file. Пожалуйста, выберите файл. - + Cannot open file '%1'. Не удалось открыть файл '%1'. - + Downloading (%1MB) Загрузка (%1MB) - + Network error: %1. Ошибка сети: %1. - + Parsing file Ожидание файла - + Xz extraction failed. - + Ошибка распаковки архива - + Sorry, this version of Oracle does not support xz compressed files. - + Данная версия Oracle не поддерживает архивы xz - + Failed to open Zip archive: %1. Не удалось загрузить zip-архив: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Ошибка распаковки: zip-архив содержит больше одного файла. - + Zip extraction failed: %1. Ошибка распаковки zip-архива: %1. - + Sorry, this version of Oracle does not support zipped files. Данная версия Oracle не поддерживает zip-архивы. - + Failed to interpret downloaded data. - + Ошибка в обработке загруженных данных - + Do you want to download the uncompressed file instead? - + Хотите вместо этого загрузить новую несжатую копию? - + The file was retrieved successfully, but it does not contain any sets data. Файл успешно получен, но в нем не содержится данных о сетах. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + Сохранить базу карт - + XML; spoiler database (*.xml) + XML; база карт (*.xml) + + + + spoiler - + Spoilers import - + Импорт карт - + Please specify a compatible source for spoiler data. - + Укажите совместимый ресурс для импорта базы карт - + Download URL: Ссылка на скачивание: - + + Local file: + + + + Restore default URL Восстановить ссылку по умолчанию - - The spoiler database will be saved at the following location: + + Choose file... - + + The spoiler database will be saved at the following location: + База карт будет сохранена в следующей директории: + + + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: Ссылка на скачивание: - + + Local file: + + + + Restore default URL Восстановить ссылку по умолчанию - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens Пример сета с фишками @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Импортер Oracle @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Ошибка - + No set has been imported. Не было импортировано ни одного сета. - + Sets imported Импортировано сетов - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save Сохранить - + Import finished: %1 cards. Импорт завершен: %1 карт. - + %1: %2 cards imported %1: %2 карт импортировано - + Save card database Сохранить базу карт - + XML; card database (*.xml) База карт (*.xml) - + The file could not be saved to %1 Не удалось сохранить файл в %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -396,87 +448,87 @@ ZIP operation completed successfully. - + Распаковка успешно завершена. Failed to initialize or load zlib library. - + Не удалось загрузить библиотеку .zlib. zlib library error. - + ошибка библиотеки .zlib Unable to create or open file. - + Не удалось создать или открыть файл. Partially corrupted archive. Some files might be extracted. - + Архив частично поврежден. Некоторые файлы могут быть извлечены. Corrupted archive. - + Архив поврежден. Wrong password. - + Неверный пароль. No archive has been created yet. - + Не было создано ни одного архива. File or directory does not exist. - + Файл или директория не существуют. File read error. - + Ошибка чтения файла. File write error. - + Ошибка записи файла. File seek error. - + Не удалось найти файл. Unable to create a directory. - + Не удалось создать директорию. Invalid device. - + Неверное устройство. Invalid or incompatible zip archive. - + Неверный или несовместимый zip-архив. Inconsistent headers. Archive might be corrupted. - + Несовместимые заголовки. Архив может быть поврежден. Unknown error. - + Неизвестная ошибка. @@ -484,58 +536,58 @@ ZIP operation completed successfully. - + Распаковка успешно завершена. Failed to initialize or load zlib library. - + Не удалось загрузить библиотеку .zlib. zlib library error. - + ошибка библиотеки .zlib Unable to create or open file. - + Не удалось создать или открыть файл. No archive has been created yet. - + Не было создано ни одного архива. File or directory does not exist. - + Файл или директория не существуют. File read error. - + Ошибка чтения файла. File write error. - + Ошибка записи файла. File seek error. - + Не удалось найти файл. Unknown error. - + Неизвестная ошибка. i18n - + English Русский (Russian) diff --git a/oracle/translations/oracle_sr.ts b/oracle/translations/oracle_sr.ts index 8c2b576dd..04d48688a 100644 --- a/oracle/translations/oracle_sr.ts +++ b/oracle/translations/oracle_sr.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Uvod - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Interface language: - + Version: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Odabir izvora - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: URL za preuzimanje: - + Local file: Lokalni fajl: - + Restore default URL Povrati uobičajeni URL - + Choose file... Izaberite fajl... - + Load sets file - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Greška - + The provided URL is not valid. Dati URL nije važeći. - + Downloading (0MB) Preuzimanje (0MB) - + Please choose a file. Molimo Vas izaberite fajl. - + Cannot open file '%1'. Nemoguće otvori fajl '%1'. - + Downloading (%1MB) Preuzimanje (%1MB) - + Network error: %1. Mrežna greška: %1. - + Parsing file Parsiranje fajla - + Xz extraction failed. - + Sorry, this version of Oracle does not support xz compressed files. - + Failed to open Zip archive: %1. Neuspeh u otvaranju Zip arhive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. Izvinite, ova verzija Oracle-a ne podržava zip fajlove. - + Failed to interpret downloaded data. - + Do you want to download the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database - + XML; spoiler database (*.xml) - + + spoiler + + + + Spoilers import - + Please specify a compatible source for spoiler data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: URL za preuzimanje: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error Greška - + No set has been imported. - + Sets imported - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + The file could not be saved to %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -426,7 +478,7 @@ Wrong password. - + Pogrešna lozinka. @@ -441,17 +493,17 @@ File read error. - + Greška u čitanju fajla. File write error. - + Greška u pisanju fajla. File seek error. - + Greška u traženju fajla. @@ -476,7 +528,7 @@ Unknown error. - + Nepoznata greška. @@ -499,7 +551,7 @@ Unable to create or open file. - + Nemoguće napraviti ili otvoriti fajl. @@ -514,28 +566,28 @@ File read error. - + Greška u čitanju fajla. File write error. - + Greška u pisanju fajla. File seek error. - + Greška u traženju fajla. Unknown error. - + Nepoznata greška. i18n - + English Srpski (Serbian) diff --git a/oracle/translations/oracle_tr.ts b/oracle/translations/oracle_tr.ts index 5b9dd2906..517f9248e 100644 --- a/oracle/translations/oracle_tr.ts +++ b/oracle/translations/oracle_tr.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction Açıklama - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. Bu sihirbaz Cockatrice tarafından kullanılacak setlerin, kartların ve tokenlerin listesini içe aktaracaktır. - + Interface language: Arayüz dili: - + Version: Sürüm: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection Kaynak seçimi - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. Lütfen set ve kart listesi için uyumlu bir kaynak belirtin. İndirilecek bir URL adresi belirtebilir veya bilgisayarınızdaki mevcut bir dosyayı kullanabilirsiniz. - + Download URL: İndirme Bağlantısı: - + Local file: Yerel dosya: - + Restore default URL Varsayılan URL'yi geri yükle - + Choose file... Dosya seç... - + Load sets file Set dosyasını yükle - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error Hata - + The provided URL is not valid. Paylaşılan bağlantı geçerli değil. - + Downloading (0MB) İndiriliyor (0MB) - + Please choose a file. Lütfen dosya seçin. - + Cannot open file '%1'. Dosya açılamıyor '1%'. - + Downloading (%1MB) İndiriliyor (%1MB) - + Network error: %1. Bağlantı hatası: %1 - + Parsing file Dosya ayrıştırılıyor - + Xz extraction failed. Xz çıkarma işlemi başarısız oldu. - + Sorry, this version of Oracle does not support xz compressed files. Maalesef, Oracle'ın bu sürümü xz sıkıştırılmış dosyaları desteklemiyor. - + Failed to open Zip archive: %1. Zip dosyası açılamadı: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. Zip çıkarma işlemi başarısız oldu: Zip arşivi tam olarak bir dosya içermiyor. - + Zip extraction failed: %1. Zip çıkarma işlemi başarısız oldu: %1. - + Sorry, this version of Oracle does not support zipped files. Maalesef, Oracle'ın bu sürümü zip dosyalarını desteklemiyor. - + Failed to interpret downloaded data. İndirilen veriler işlenemedi. - + Do you want to download the uncompressed file instead? Bunun yerine sıkıştırılmamış dosyayı indirmek ister misiniz? - + The file was retrieved successfully, but it does not contain any sets data. Dosya başarıyla alındı, ancak herhangi bir set verisi içermiyor. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database Spoiler veritabanını kaydet - + XML; spoiler database (*.xml) XML; spoiler veritabanı (*.xml) - + + spoiler + + + + Spoilers import Spoilers içe aktar - + Please specify a compatible source for spoiler data. Lütfen spoiler verileri için uyumlu bir kaynak belirtin. - + Download URL: İndirme Bağlantısı: - + + Local file: + + + + Restore default URL Varsayılan bağlantıyı geri yükle - + + Choose file... + + + + The spoiler database will be saved at the following location: - + Save to a custom path (not recommended) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database - + XML; token database (*.xml) - + + tokens + + + + Tokens import - + Please specify a compatible source for token data. - + Download URL: - + + Local file: + + + + Restore default URL - + + Choose file... + + + + The token database will be saved at the following location: - + Save to a custom path (not recommended) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer @@ -262,22 +292,22 @@ OutroPage - + Finished - + The wizard has finished. - + You can now start using Cockatrice with the newly updated cards. - + If the card databases don't reload automatically, restart the Cockatrice client. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error - + No set has been imported. - + Sets imported - + A cockatrice database file of %1 MB has been downloaded. - + The following sets have been found: - + Press "Save" to store the imported cards in the Cockatrice database. - + The card database will be saved at the following location: - + Save to a custom path (not recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + The file could not be saved to %1 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error - + The provided URL is not valid: - + Downloading (0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) - + Network error: %1. - + The file could not be saved to %1 @@ -535,7 +587,7 @@ i18n - + English Türkçe (Turkish) diff --git a/oracle/translations/oracle_yue.ts b/oracle/translations/oracle_yue.ts index dded76f34..13a621b9e 100644 --- a/oracle/translations/oracle_yue.ts +++ b/oracle/translations/oracle_yue.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction 介绍 - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. 此嚮導將會導入Cockatrice中用到的系列,卡牌和衍生物列表。 - + Interface language: 介面語言: - + Version: 版本: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection 選擇來源 - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. 請選擇合適的牌組和卡片清單來源。您可以輸入下載URL或使用電腦中已有的檔案。 - + Download URL: 下载URL: - + Local file: 本地檔案: - + Restore default URL 恢復常用URL - + Choose file... 選擇檔案... - + Load sets file 载入牌组檔案 - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error 出現錯誤 - + The provided URL is not valid. 所提供的url無效. - + Downloading (0MB) 下載中 (0MB) - + Please choose a file. 請選擇檔案 - + Cannot open file '%1'. 不能開啟檔案 '%1 - + Downloading (%1MB) 下載中 (%1MB) - + Network error: %1. 網路出錯: %1. - + Parsing file 分析檔案中 - + Xz extraction failed. xz解壓縮失敗. - + Sorry, this version of Oracle does not support xz compressed files. 很抱歉,現前Oracle版本不支援xc壓縮檔案 - + Failed to open Zip archive: %1. 未能開啟壓縮文件: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. 解壓縮失敗:這壓縮文件擁有超過一個檔案. - + Zip extraction failed: %1. 解壓縮失敗: %1. - + Sorry, this version of Oracle does not support zipped files. 很抱歉,現前Oracle版本不支援壓縮檔案. - + Failed to interpret downloaded data. 不能分析下載資料. - + Do you want to download the uncompressed file instead? 你是否想另再下載未經壓縮的檔案? - + The file was retrieved successfully, but it does not contain any sets data. 雖然檔案成功取回, 但是檔案並未含有任何牌組資料. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database 儲存預覽資料庫 - + XML; spoiler database (*.xml) XML ; 預覽資料庫(*.XML) - + + spoiler + + + + Spoilers import 導入預覽卡牌 - + Please specify a compatible source for spoiler data. 請指定預覽資料合用來源. - + Download URL: 下载URL: - + + Local file: + + + + Restore default URL 恢復常用URL - + + Choose file... + + + + The spoiler database will be saved at the following location: 預覽資料庫將會儲藏於以下位置 - + Save to a custom path (not recommended) 儲存到自訂路徑(不建議) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database 儲存衍生物資料庫 - + XML; token database (*.xml) XML; 衍生物資料庫 (*.xml) - + + tokens + + + + Tokens import 導入衍生物 - + Please specify a compatible source for token data. 請指定衍生物資料合用來源. - + Download URL: 下载URL: - + + Local file: + + + + Restore default URL 恢復常用URL - + + Choose file... + + + + The token database will be saved at the following location: 衍生物資料庫將會儲藏於以下位置: - + Save to a custom path (not recommended) 儲存到自訂路徑(不建議) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens 包含衍生物的虚拟牌組 @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle導入器 @@ -262,22 +292,22 @@ OutroPage - + Finished 完成 - + The wizard has finished. 嚮導程序工作完成 - + You can now start using Cockatrice with the newly updated cards. Cockatrice已載入更新了卡牌, 現在已經可以使用. - + If the card databases don't reload automatically, restart the Cockatrice client. 如果卡牌資料庫尚未能自動更新,請把cockatrice用戶端重新開啟. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error 出現錯誤 - + No set has been imported. 沒有牌組被導入。 - + Sets imported 已導入牌組 - + A cockatrice database file of %1 MB has been downloaded. 有%1MB大小的Cockatrice資料庫檔案已被下載 - + The following sets have been found: 已找到以下牌组: - + Press "Save" to store the imported cards in the Cockatrice database. 要將導入卡牌保存於Cockatrice資料庫內請按"儲藏". - + The card database will be saved at the following location: 卡牌資料庫將被儲藏於以下位置 - + Save to a custom path (not recommended) 儲存到自訂路徑(不建議) - + &Save &儲存 - + Import finished: %1 cards. 導入過程完成:成功導入%1張卡牌. - + %1: %2 cards imported %1:成功導入%2張卡牌. - + Save card database 儲存卡牌資料庫 - + XML; card database (*.xml) XML; 卡牌資料庫 (*.xml) - + The file could not be saved to %1 檔案未能儲藏於%1。 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error 出現錯誤 - + The provided URL is not valid: - + Downloading (0MB) 下載中(0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) 下載中 (%1MB) - + Network error: %1. 網路出錯: %1. - + The file could not be saved to %1 檔案未能儲藏於%1 @@ -535,7 +587,7 @@ i18n - + English diff --git a/oracle/translations/oracle_zh-Hans.ts b/oracle/translations/oracle_zh-Hans.ts index 1087225f3..ff19ab463 100644 --- a/oracle/translations/oracle_zh-Hans.ts +++ b/oracle/translations/oracle_zh-Hans.ts @@ -2,22 +2,22 @@ IntroPage - + Introduction 介绍 - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. 此向导将会导入Cockatrice中用到的系列,卡牌和衍生物列表. - + Interface language: 界面语言: - + Version: 版本: @@ -25,134 +25,134 @@ LoadSetsPage - + Source selection 选择来源 - + Please specify a compatible source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. 请选择合适的牌组和卡片清单来源。您可以输入下载URL或使用电脑中已有的档案。 - + Download URL: 下载URL: - + Local file: 本地文件: - + Restore default URL 恢复常用URL - + Choose file... 选择档案... - + Load sets file 载入牌组档案 - + Sets file (%1) Sets JSON file (%1) - - - - - - - + + + + + + + Error 出现错误 - + The provided URL is not valid. 所提供的url无效. - + Downloading (0MB) 下载中(0MB) - + Please choose a file. 请选择档案. - + Cannot open file '%1'. 不能开启档案 '%1 - + Downloading (%1MB) 下载中(%1MB) - + Network error: %1. 网路出错: %1. - + Parsing file 分析档案中 - + Xz extraction failed. xz解压缩失败。 - + Sorry, this version of Oracle does not support xz compressed files. 很抱歉,现前Oracle版本不支援xc压缩档案 - + Failed to open Zip archive: %1. 未能开启压缩文件: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. 解压缩失败:这压缩文件拥有超过一个档案. - + Zip extraction failed: %1. 解压缩失败:%1。 - + Sorry, this version of Oracle does not support zipped files. 很抱歉,现前Oracle版本不支援压缩档案. - + Failed to interpret downloaded data. 不能分析下载资料. - + Do you want to download the uncompressed file instead? 你是否想另再下载未经压缩的档案? - + The file was retrieved successfully, but it does not contain any sets data. 虽然档案成功取回, 但是档案并未含有任何牌组资料. @@ -160,42 +160,57 @@ LoadSpoilersPage - + Save spoiler database 储存预览资料库 - + XML; spoiler database (*.xml) XML ; 预览资料库(*.XML) - + + spoiler + + + + Spoilers import 导入预览卡牌 - + Please specify a compatible source for spoiler data. 请指定预览资料合用来源. - + Download URL: 下载URL: - + + Local file: + + + + Restore default URL 恢复常用URL - + + Choose file... + + + + The spoiler database will be saved at the following location: 预览资料库将会储藏于以下位置: - + Save to a custom path (not recommended) 保存到自定义路径(不推荐) @@ -203,42 +218,57 @@ LoadTokensPage - + Save token database 储存衍生物资料库 - + XML; token database (*.xml) XML; 衍生物资料库 (*.xml) - + + tokens + + + + Tokens import 导入衍生物 - + Please specify a compatible source for token data. 请指定衍生物资料合用来源. - + Download URL: 下载URL: - + + Local file: + + + + Restore default URL 恢复常用URL - + + Choose file... + + + + The token database will be saved at the following location: 衍生物资料库将会储藏于以下位置: - + Save to a custom path (not recommended) 保存到自定义路径(不推荐) @@ -246,7 +276,7 @@ OracleImporter - + Dummy set containing tokens 包含衍生物的虚拟牌组 @@ -254,7 +284,7 @@ OracleWizard - + Oracle Importer Oracle导入器 @@ -262,22 +292,22 @@ OutroPage - + Finished 完成 - + The wizard has finished. 向导程式工作完成. - + You can now start using Cockatrice with the newly updated cards. Cockatrice已载入更新了卡牌, 现在已经可以使用. - + If the card databases don't reload automatically, restart the Cockatrice client. 如果卡牌资料库尚未能自动更新,请把cockatrice用户端重新开启. @@ -285,73 +315,73 @@ SaveSetsPage - - + + Error 出现错误 - + No set has been imported. 没有牌组被导入。 - + Sets imported 已导入牌组 - + A cockatrice database file of %1 MB has been downloaded. 有%1MB大小的Cockatrice资料库档案已被下载. - + The following sets have been found: 已找到以下牌组: - + Press "Save" to store the imported cards in the Cockatrice database. 要将导入卡牌保存于Cockatrice资料库内请按"储藏". - + The card database will be saved at the following location: 卡牌资料库将被储藏于以下位置 - + Save to a custom path (not recommended) 保存到自定义路径(不推荐) - + &Save &储存 - + Import finished: %1 cards. 导入过程完成:成功导入%1张卡牌. - + %1: %2 cards imported %1:成功导入%2张卡牌. - + Save card database 储存卡牌资料库 - + XML; card database (*.xml) XML; 卡牌资料库 (*.xml) - + The file could not be saved to %1 档案未能储藏于%1。 @@ -359,34 +389,56 @@ SimpleDownloadFilePage - - - + + Load %1 file + + + + + %1 file (%1) + + + + + + + + Error 出现错误 - + The provided URL is not valid: - + Downloading (0MB) 下载中(0MB) - + + Please choose a file. + + + + + Cannot open file '%1'. + + + + Downloading (%1MB) 下载中(%1MB) - + Network error: %1. 网路出错: %1. - + The file could not be saved to %1 档案未能储藏于%1 @@ -535,7 +587,7 @@ i18n - + English 简体中文 (Chinese Simplified) diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 2b345362a..6e4191beb 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -96,19 +96,18 @@ set(DESKTOPDIR CACHE STRING "desktop file destination" ) -# Include directories -include_directories(../common) -include_directories(${PROTOBUF_INCLUDE_DIR}) -include_directories(${CMAKE_CURRENT_BINARY_DIR}/../common) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) - # Build servatrice binary and link it add_executable(servatrice MACOSX_BUNDLE ${servatrice_MOC_SRCS} ${servatrice_RESOURCES_RCC} ${servatrice_SOURCES}) if(CMAKE_HOST_SYSTEM MATCHES "FreeBSD") - target_link_libraries(servatrice cockatrice_common Threads::Threads ${SERVATRICE_QT_MODULES} ${LIBEXECINFO_LIBRARY}) + target_link_libraries( + servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} + ${LIBEXECINFO_LIBRARY} + ) else() - target_link_libraries(servatrice cockatrice_common Threads::Threads ${SERVATRICE_QT_MODULES}) + target_link_libraries( + servatrice libcockatrice_deck_list libcockatrice_network_server_remote Threads::Threads ${SERVATRICE_QT_MODULES} + ) endif() # install rules diff --git a/servatrice/check_schema_version.sh b/servatrice/check_schema_version.sh index ed8772349..c4aadf356 100755 --- a/servatrice/check_schema_version.sh +++ b/servatrice/check_schema_version.sh @@ -6,6 +6,7 @@ version_line="$(grep 'INSERT INTO cockatrice_schema_version' servatrice/servatri version_line="${version_line#*VALUES(}" declare -i schema_ver="${version_line%%)*}" +# shellcheck disable=2012 latest_migration="$(ls -1 servatrice/migrations/ | tail -n1)" xtoysql="${latest_migration#servatrice_}" xtoy="${xtoysql%.sql}" @@ -23,7 +24,7 @@ if ((schema_ver != new_ver)); then fi expected_sql="^UPDATE cockatrice_schema_version SET version=${new_ver} WHERE version=${old_ver};$" -if ! grep -q "$expected_sql" servatrice/migrations/$latest_migration; then +if ! grep -q "$expected_sql" "servatrice/migrations/$latest_migration"; then echo "$latest_migration does not contain expected sql: $expected_sql" exit 1 fi diff --git a/servatrice/scripts/mk_pypb.sh b/servatrice/scripts/mk_pypb.sh deleted file mode 100755 index e11d237d2..000000000 --- a/servatrice/scripts/mk_pypb.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -SRC_DIR=../../common/pb/ -DST_DIR=./pypb - -rm -rf "$DST_DIR" -mkdir -p "$DST_DIR" -protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/*.proto -touch "$DST_DIR/__init__.py" - diff --git a/servatrice/src/email_parser.cpp b/servatrice/src/email_parser.cpp index ebb9cece3..d8c30d852 100644 --- a/servatrice/src/email_parser.cpp +++ b/servatrice/src/email_parser.cpp @@ -25,7 +25,7 @@ QPair EmailParser::parseEmailAddress(const QString &dirtyEmail // Trim out dots and pluses from Google/Gmail domains if (capturedEmailAddressDomain.toLower() == "gmail.com") { - // Remove all content after first plus sign (as unnecessary with gmail) + // Remove all content after the first plus sign (as unnecessary with gmail) // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html const auto firstPlusSign = capturedEmailUser.indexOf("+"); if (firstPlusSign != -1) { @@ -36,6 +36,13 @@ QPair EmailParser::parseEmailAddress(const QString &dirtyEmail // https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html capturedEmailUser.replace(".", ""); } + // Trim out minuses from Yahoo domains + else if (capturedEmailAddressDomain.toLower() == "yahoo.com") { + const auto firstMinusSign = capturedEmailUser.indexOf("-"); + if (firstMinusSign != -1) { + capturedEmailUser = capturedEmailUser.left(firstMinusSign); + } + } return {capturedEmailUser, capturedEmailAddressDomain}; } diff --git a/servatrice/src/isl_interface.cpp b/servatrice/src/isl_interface.cpp index 351c608c5..692a0fdba 100644 --- a/servatrice/src/isl_interface.cpp +++ b/servatrice/src/isl_interface.cpp @@ -1,25 +1,28 @@ #include "isl_interface.h" -#include "debug_pb_message.h" -#include "get_pb_extension.h" #include "main.h" -#include "pb/event_game_joined.pb.h" -#include "pb/event_join_room.pb.h" -#include "pb/event_leave_room.pb.h" -#include "pb/event_list_games.pb.h" -#include "pb/event_remove_messages.pb.h" -#include "pb/event_room_say.pb.h" -#include "pb/event_server_complete_list.pb.h" -#include "pb/event_user_joined.pb.h" -#include "pb/event_user_left.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/isl_message.pb.h" #include "server_logger.h" -#include "server_protocolhandler.h" -#include "server_room.h" +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(IslInterfaceLog, "isl_interface"); void IslInterface::sharedCtor(const QSslCertificate &cert, const QSslKey &privateKey) { @@ -112,15 +115,12 @@ void IslInterface::initServer() socket->startServerEncryption(); if (!socket->waitForEncrypted(5000)) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) QList sslErrors(socket->sslHandshakeErrors()); -#else - QList sslErrors(socket->sslErrors()); -#endif - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } deleteLater(); return; } @@ -161,7 +161,7 @@ void IslInterface::initServer() server->islLock.lockForWrite(); if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; deleteLater(); } else { transmitMessage(message); @@ -184,31 +184,28 @@ void IslInterface::initClient() expectedErrors.append(QSslError(QSslError::SelfSignedCertificate, peerCert)); socket->ignoreSslErrors(expectedErrors); - qDebug() << "[ISL] Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; + qCDebug(IslInterfaceLog) << "Connecting to #" << serverId << ":" << peerAddress << ":" << peerPort; socket->connectToHostEncrypted(peerAddress, peerPort, peerHostName); if (!socket->waitForConnected(5000)) { - qDebug() << "[ISL] Socket error:" << socket->errorString(); + qCDebug(IslInterfaceLog) << "Socket error:" << socket->errorString(); deleteLater(); return; } if (!socket->waitForEncrypted(5000)) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) QList sslErrors(socket->sslHandshakeErrors()); -#else - QList sslErrors(socket->sslErrors()); -#endif - if (sslErrors.isEmpty()) - qDebug() << "[ISL] SSL handshake timeout, terminating connection"; - else - qDebug() << "[ISL] SSL errors:" << sslErrors; + if (sslErrors.isEmpty()) { + qCDebug(IslInterfaceLog) << "SSL handshake timeout, terminating connection"; + } else { + qCWarning(IslInterfaceLog) << "SSL errors:" << sslErrors; + } deleteLater(); return; } server->islLock.lockForWrite(); if (server->islConnectionExists(serverId)) { - qDebug() << "[ISL] Duplicate connection to #" << serverId << "terminating connection"; + qCDebug(IslInterfaceLog) << "Duplicate connection to #" << serverId << "terminating connection"; deleteLater(); return; } @@ -250,17 +247,21 @@ void IslInterface::readClient() return; IslMessage newMessage; - newMessage.ParseFromArray(inputBuffer.data(), messageLength); + bool ok = newMessage.ParseFromArray(inputBuffer.data(), messageLength); inputBuffer.remove(0, messageLength); messageInProgress = false; - processMessage(newMessage); + if (ok) { + processMessage(newMessage); + } else { + qCWarning(IslInterfaceLog) << "parsing error!"; + } } while (!inputBuffer.isEmpty()); } void IslInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - qDebug() << "[ISL] Socket error:" << socketError; + qCWarning(IslInterfaceLog) << "Socket error:" << socketError; server->islLock.lockForWrite(); server->removeIslInterface(serverId); @@ -278,7 +279,10 @@ void IslInterface::transmitMessage(const IslMessage &item) unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); + if (!item.SerializeToArray(buf.data() + 4, size)) { + qCWarning(IslInterfaceLog) << "transmit error!"; + return; + } buf.data()[3] = (unsigned char)size; buf.data()[2] = (unsigned char)(size >> 8); buf.data()[1] = (unsigned char)(size >> 16); @@ -376,7 +380,7 @@ void IslInterface::processSessionEvent(const SessionEvent &event, qint64 session QReadLocker clientsLocker(&server->clientsLock); Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + qCDebug(IslInterfaceLog) << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; break; } const Event_GameJoined &gameJoined = event.GetExtension(Event_GameJoined::ext); @@ -390,7 +394,8 @@ void IslInterface::processSessionEvent(const SessionEvent &event, qint64 session QReadLocker clientsLocker(&server->clientsLock); Server_AbstractUserInterface *client = server->getUsersBySessionId().value(sessionId); if (!client) { - qDebug() << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; + qCWarning(IslInterfaceLog) + << "IslInterface::processSessionEvent: session id" << sessionId << "not found"; break; } @@ -438,7 +443,7 @@ void IslInterface::processRoomCommand(const CommandContainer &cont, qint64 sessi void IslInterface::processMessage(const IslMessage &item) { - qDebug() << getSafeDebugString(item); + qCDebug(IslInterfaceLog) << getSafeDebugString(item); switch (item.message_type()) { case IslMessage::ROOM_COMMAND_CONTAINER: { diff --git a/servatrice/src/isl_interface.h b/servatrice/src/isl_interface.h index 19310e07b..27e5b8293 100644 --- a/servatrice/src/isl_interface.h +++ b/servatrice/src/isl_interface.h @@ -1,13 +1,13 @@ #ifndef ISL_INTERFACE_H #define ISL_INTERFACE_H -#include "pb/serverinfo_game.pb.h" -#include "pb/serverinfo_room.pb.h" -#include "pb/serverinfo_user.pb.h" #include "servatrice.h" #include #include +#include +#include +#include class Servatrice; class QSslSocket; diff --git a/servatrice/src/main.cpp b/servatrice/src/main.cpp index c8950d973..b1294a04c 100644 --- a/servatrice/src/main.cpp +++ b/servatrice/src/main.cpp @@ -18,8 +18,6 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#include "passwordhasher.h" -#include "rng_sfmt.h" #include "servatrice.h" #include "server_logger.h" #include "settingscache.h" @@ -34,6 +32,8 @@ #include #include #include +#include +#include RNG_Abstract *rng; ServerLogger *logger; diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 619586dad..410bf4ed9 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -19,18 +19,12 @@ ***************************************************************************/ #include "servatrice.h" -#include "decklist.h" #include "email_parser.h" -#include "featureset.h" #include "isl_interface.h" #include "main.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_server_shutdown.pb.h" #include "servatrice_connection_pool.h" #include "servatrice_database_interface.h" #include "server_logger.h" -#include "server_room.h" #include "serversocketinterface.h" #include "settingscache.h" #include "smtpclient.h" @@ -45,6 +39,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include Servatrice_GameServer::Servatrice_GameServer(Servatrice *_server, int _numberPools, @@ -254,13 +254,8 @@ bool Servatrice::initServer() qDebug() << "Accept registered users only:" << getRegOnlyServerEnabled(); qDebug() << "Registration enabled:" << getRegistrationEnabled(); if (getRegistrationEnabled()) { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList emailBlackListFilters = getEmailBlackList().split(",", Qt::SkipEmptyParts); QStringList emailWhiteListFilters = getEmailWhiteList().split(",", Qt::SkipEmptyParts); -#else - QStringList emailBlackListFilters = getEmailBlackList().split(",", QString::SkipEmptyParts); - QStringList emailWhiteListFilters = getEmailWhiteList().split(",", QString::SkipEmptyParts); -#endif qDebug() << "Email blacklist:" << emailBlackListFilters; qDebug() << "Email whitelist:" << emailWhiteListFilters; qDebug() << "Require email address to register:" << getRequireEmailForRegistrationEnabled(); @@ -564,11 +559,7 @@ void Servatrice::setRequiredFeatures(const QString &featureList) FeatureSet features; serverRequiredFeatureList.clear(); features.initalizeFeatureList(serverRequiredFeatureList); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList listReqFeatures = featureList.split(",", Qt::SkipEmptyParts); -#else - QStringList listReqFeatures = featureList.split(",", QString::SkipEmptyParts); -#endif if (!listReqFeatures.isEmpty()) for (const QString &reqFeature : listReqFeatures) { features.enableRequiredFeature(serverRequiredFeatureList, reqFeature); diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 8dbaf492f..62fb382cb 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -20,8 +20,6 @@ #ifndef SERVATRICE_H #define SERVATRICE_H -#include "server.h" - #include #include #include @@ -31,6 +29,7 @@ #include #include #include +#include #include Q_DECLARE_METATYPE(QSqlDatabase) @@ -284,4 +283,4 @@ public: QList getServerList() const; }; -#endif \ No newline at end of file +#endif diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 2a8b9d675..bce3542e8 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1,8 +1,5 @@ #include "servatrice_database_interface.h" -#include "decklist.h" -#include "passwordhasher.h" -#include "pb/game_replay.pb.h" #include "servatrice.h" #include "serversocketinterface.h" #include "settingscache.h" @@ -10,8 +7,14 @@ #include #include #include +#include #include #include +#include +#include +#include + +inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface"); Servatrice_DatabaseInterface::Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server) : instanceId(_instanceId), sqlDatabase(QSqlDatabase()), server(_server) @@ -56,17 +59,16 @@ bool Servatrice_DatabaseInterface::openDatabase() sqlDatabase.close(); const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qDebug().noquote() << QString("[%1] Opening database...").arg(poolStr); + qCDebug(DatabaseInterfaceLog).noquote() << poolStr << "Opening database..."; if (!sqlDatabase.open()) { - qCritical() << QString("[%1] Error opening database: %2").arg(poolStr).arg(sqlDatabase.lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database:" << sqlDatabase.lastError().text(); return false; } QSqlQuery *versionQuery = prepareQuery("select version from {prefix}_schema_version limit 1"); if (!execSqlQuery(versionQuery)) { - qCritical() << QString("[%1] Error opening database: unable to load database schema version (hint: ensure the " - "cockatrice_schema_version exists)") - .arg(poolStr); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: unable to load database schema version" + << "(hint: ensure the cockatrice_schema_version exists)"; return false; } @@ -74,24 +76,21 @@ bool Servatrice_DatabaseInterface::openDatabase() const int dbversion = versionQuery->value(0).toInt(); const int expectedversion = DATABASE_SCHEMA_VERSION; if (dbversion < expectedversion) { - qCritical() << QString("[%1] Error opening database: the database schema version is too old, you need to " - "run the migrations to update it from version %2 to version %3") - .arg(poolStr) - .arg(dbversion) - .arg(expectedversion); + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: the database schema version is too old, you " + "need to run the migrations to update it from version" + << dbversion << "to version" << expectedversion; return false; } else if (dbversion > expectedversion) { - qCritical() << QString("[%1] Error opening database: the database schema version %2 is too new, you need " - "to update servatrice (this servatrice actually uses version %3)") - .arg(poolStr) - .arg(dbversion) - .arg(expectedversion); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: the database schema version" + << dbversion << "is too new, you need to update servatrice" + << "(this servatrice actually uses version" << expectedversion << ")"; return false; } } else { - qCritical() << QString("[%1] Error opening database: unable to load database schema version (hint: ensure the " - "cockatrice_schema_version contains a single record)") - .arg(poolStr); + qCCritical(DatabaseInterfaceLog) << poolStr + << "Error opening database: unable to load database schema version (hint: " + "ensure the cockatrice_schema_version contains a single record)"; return false; } @@ -114,9 +113,7 @@ bool Servatrice_DatabaseInterface::checkSql() if (query.lastError().isValid()) { const auto &poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qCritical() << QString("[%1] Error executing query: %2, resetting connection") - .arg(poolStr) - .arg(query.lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query.lastError().text(); sqlDatabase.close(); return openDatabase(); @@ -145,7 +142,7 @@ bool Servatrice_DatabaseInterface::execSqlQuery(QSqlQuery *query) if (query->exec()) return true; const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId); - qCritical() << QString("[%1] Error executing query: %2").arg(poolStr).arg(query->lastError().text()); + qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query->lastError().text(); sqlDatabase.close(); openDatabase(); return false; @@ -163,11 +160,7 @@ bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user, QString bool allowPunctuationPrefix = settingsCache->value("users/allowpunctuationprefix", false).toBool(); QString allowedPunctuation = settingsCache->value("users/allowedpunctuation", "_").toString(); QString disallowedWordsStr = settingsCache->value("users/disallowedwords", "").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList disallowedWords = disallowedWordsStr.split(",", Qt::SkipEmptyParts); -#else - QStringList disallowedWords = disallowedWordsStr.split(",", QString::SkipEmptyParts); -#endif disallowedWords.removeDuplicates(); QVariant displayDisallowedWords = settingsCache->value("users/displaydisallowedwords"); QString disallowedRegExpStr; @@ -256,7 +249,8 @@ bool Servatrice_DatabaseInterface::registerUser(const QString &userName, query->bindValue(":token", token); if (!execSqlQuery(query)) { - qDebug() << "Failed to insert user: " << query->lastError() << " sql: " << query->lastQuery(); + qCWarning(DatabaseInterfaceLog) << "Failed to insert user: " << query->lastError() + << " sql: " << query->lastQuery(); return false; } @@ -274,8 +268,8 @@ bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const Q activateQuery->bindValue(":username", userName); activateQuery->bindValue(":token", token); if (!execSqlQuery(activateQuery)) { - qDebug() << "Account activation failed: SQL error." << activateQuery->lastError() - << " sql: " << activateQuery->lastQuery(); + qCWarning(DatabaseInterfaceLog) << "Account activation failed: SQL error." << activateQuery->lastError() + << " sql: " << activateQuery->lastQuery(); return false; } @@ -288,7 +282,8 @@ bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const Q query->bindValue(":userName", userName); if (!execSqlQuery(query)) { - qDebug() << "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery(); + qCWarning(DatabaseInterfaceLog) + << "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery(); return false; } @@ -330,7 +325,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot prepareQuery("select password_sha512, active from {prefix}_users where name = :name"); passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { - qDebug("Login denied: SQL error"); + qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error"; return NotLoggedIn; } @@ -338,7 +333,7 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot const QString correctPasswordSha512 = passwordQuery->value(0).toString(); const bool userIsActive = passwordQuery->value(1).toBool(); if (!userIsActive) { - qDebug("Login denied: user not active"); + qCWarning(DatabaseInterfaceLog) << "Login denied: user not active"; return UserIsInactive; } QString hashedPassword; @@ -348,14 +343,14 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot hashedPassword = password; } if (correctPasswordSha512 == hashedPassword) { - qDebug("Login accepted: password right"); + qCDebug(DatabaseInterfaceLog) << "Login accepted: password right"; return PasswordRight; } else { - qDebug("Login denied: password wrong"); + qCDebug(DatabaseInterfaceLog) << "Login denied: password wrong"; return NotLoggedIn; } } else { - qDebug("Login accepted: unknown user"); + qCDebug(DatabaseInterfaceLog) << "Login accepted: unknown user"; return UnknownUser; } } @@ -373,7 +368,7 @@ bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress, return false; if (!checkSql()) { - qDebug("Failed to check if user is banned. Database invalid."); + qCWarning(DatabaseInterfaceLog) << "Failed to check if user is banned. Database invalid."; return false; } @@ -404,7 +399,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, idBanQuery->bindValue(":id", clientId); idBanQuery->bindValue(":id2", clientId); if (!execSqlQuery(idBanQuery)) { - qDebug() << "Id ban check failed: SQL error." << idBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "Id ban check failed: SQL error." << idBanQuery->lastError(); return false; } @@ -414,7 +409,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId, if ((secondsLeft > 0) || permanentBan) { banReason = idBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "User is banned by client id" << clientId; + qCDebug(DatabaseInterfaceLog) << "User is banned by client id" << clientId; return true; } } @@ -432,7 +427,7 @@ bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName nameBanQuery->bindValue(":name1", userName); nameBanQuery->bindValue(":name2", userName); if (!execSqlQuery(nameBanQuery)) { - qDebug() << "Name ban check failed: SQL error" << nameBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "Name ban check failed: SQL error" << nameBanQuery->lastError(); return false; } @@ -442,7 +437,7 @@ bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName if ((secondsLeft > 0) || permanentBan) { banReason = nameBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "Username" << userName << "is banned by name"; + qCDebug(DatabaseInterfaceLog) << "Username" << userName << "is banned by name"; return true; } } @@ -468,7 +463,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, ipBanQuery->bindValue(":address", ipAddress); ipBanQuery->bindValue(":address2", ipAddress); if (!execSqlQuery(ipBanQuery)) { - qDebug() << "IP ban check failed: SQL error." << ipBanQuery->lastError(); + qCWarning(DatabaseInterfaceLog) << "IP ban check failed: SQL error." << ipBanQuery->lastError(); return false; } @@ -478,7 +473,7 @@ bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress, if ((secondsLeft > 0) || permanentBan) { banReason = ipBanQuery->value(2).toString(); banSecondsRemaining = permanentBan ? 0 : secondsLeft; - qDebug() << "User is banned by address" << ipAddress; + qCDebug(DatabaseInterfaceLog) << "User is banned by address" << ipAddress; return true; } } @@ -869,12 +864,17 @@ void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName, const unsigned int size = static_cast(replayList[i]->ByteSize()); #endif blob.resize(size); - replayList[i]->SerializeToArray(blob.data(), size); + qulonglong replayId = replayList[i]->replay_id(); + if (replayList[i]->SerializeToArray(blob.data(), size)) { - replayIds.append(QVariant((qulonglong)replayList[i]->replay_id())); - replayGameIds.append(gameInfo.game_id()); - replayDurations.append(replayList[i]->duration_seconds()); - replayBlobs.append(blob); + replayIds.append(QVariant(replayId)); + replayGameIds.append(gameInfo.game_id()); + replayDurations.append(replayList[i]->duration_seconds()); + replayBlobs.append(blob); + } else { + qCWarning(DatabaseInterfaceLog) + << "failed to serialise replay, id:" << replayId << "game:" << gameInfo.game_id(); + } } { @@ -1021,7 +1021,7 @@ bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user, passwordQuery->bindValue(":name", user); if (!execSqlQuery(passwordQuery)) { - qDebug("Change password denied: SQL error"); + qCWarning(DatabaseInterfaceLog) << "Change password denied: SQL error"; return false; } @@ -1088,7 +1088,7 @@ void Servatrice_DatabaseInterface::updateUsersLastLoginData(const QString &userN QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :user_name"); query->bindValue(":user_name", userName); if (!execSqlQuery(query)) { - qDebug("Failed to locate user id when updating users last login data: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to locate user id when updating users last login data: SQL Error"; return; } @@ -1137,7 +1137,7 @@ QList Servatrice_DatabaseInterface::getUserBanHistory(const QStr query->bindValue(":user_name", userName); if (!execSqlQuery(query)) { - qDebug("Failed to collect ban history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect ban history information: SQL Error"; return results; } @@ -1172,7 +1172,7 @@ bool Servatrice_DatabaseInterface::addWarning(const QString userName, query->bindValue(":warn_reason", warningReason); query->bindValue(":client_id", clientID); if (!execSqlQuery(query)) { - qDebug("Failed to collect create warning history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect create warning history information: SQL Error"; return false; } @@ -1193,7 +1193,7 @@ QList Servatrice_DatabaseInterface::getUserWarnHistory(const query->bindValue(":user_id", userID); if (!execSqlQuery(query)) { - qDebug("Failed to collect warning history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect warning history information: SQL Error"; return results; } @@ -1300,7 +1300,7 @@ QList Servatrice_DatabaseInterface::getMessageLogHistory } if (!execSqlQuery(query)) { - qDebug("Failed to collect log history information: SQL Error"); + qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error"; return results; } @@ -1328,7 +1328,8 @@ int Servatrice_DatabaseInterface::checkNumberOfUserAccounts(const QString &email query->bindValue(":user_email", email); if (!execSqlQuery(query)) { - qDebug("Failed to identify the number of users accounts for users email address: SQL Error"); + qCWarning(DatabaseInterfaceLog) + << "Failed to identify the number of users accounts for users email address: SQL Error"; return 0; } diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index e3846cdd4..68080404c 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -1,13 +1,14 @@ #ifndef SERVATRICE_DATABASE_INTERFACE_H #define SERVATRICE_DATABASE_INTERFACE_H -#include "server.h" -#include "server_database_interface.h" - #include #include #include #include +#include +#include +#include +#include #define DATABASE_SCHEMA_VERSION 34 diff --git a/servatrice/src/server_logger.cpp b/servatrice/src/server_logger.cpp index 9e40ac7b4..de0befacb 100644 --- a/servatrice/src/server_logger.cpp +++ b/servatrice/src/server_logger.cpp @@ -57,11 +57,7 @@ void ServerLogger::logMessage(const QString &message, void *caller) // filter out all log entries based on values in configuration file bool shouldWeWriteLog = settingsCache->value("server/writelog", 1).toBool(); QString logFilters = settingsCache->value("server/logfilters").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList listlogFilters = logFilters.split(",", Qt::SkipEmptyParts); -#else - QStringList listlogFilters = logFilters.split(",", QString::SkipEmptyParts); -#endif bool shouldWeSkipLine = false; if (!shouldWeWriteLog) @@ -120,7 +116,9 @@ void ServerLogger::rotateLogs() flushBuffer(); logFile->close(); - logFile->open(QIODevice::Append); + if (!logFile->open(QIODevice::Append)) { + std::cerr << "ERROR: Failed to open log file for writing!" << std::endl; + } } QFile *ServerLogger::logFile; diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bccefc438..41e61ddec 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -20,65 +20,74 @@ #include "serversocketinterface.h" -#include "decklist.h" #include "email_parser.h" #include "main.h" -#include "pb/command_deck_del.pb.h" -#include "pb/command_deck_del_dir.pb.h" -#include "pb/command_deck_download.pb.h" -#include "pb/command_deck_list.pb.h" -#include "pb/command_deck_new_dir.pb.h" -#include "pb/command_deck_upload.pb.h" -#include "pb/command_replay_delete_match.pb.h" -#include "pb/command_replay_download.pb.h" -#include "pb/command_replay_list.pb.h" -#include "pb/command_replay_modify_match.pb.h" -#include "pb/commands.pb.h" -#include "pb/event_add_to_list.pb.h" -#include "pb/event_connection_closed.pb.h" -#include "pb/event_notify_user.pb.h" -#include "pb/event_remove_from_list.pb.h" -#include "pb/event_server_identification.pb.h" -#include "pb/event_server_message.pb.h" -#include "pb/event_user_message.pb.h" -#include "pb/response_ban_history.pb.h" -#include "pb/response_deck_download.pb.h" -#include "pb/response_deck_list.pb.h" -#include "pb/response_deck_upload.pb.h" -#include "pb/response_forgotpasswordrequest.pb.h" -#include "pb/response_get_admin_notes.pb.h" -#include "pb/response_password_salt.pb.h" -#include "pb/response_register.pb.h" -#include "pb/response_replay_download.pb.h" -#include "pb/response_replay_list.pb.h" -#include "pb/response_viewlog_history.pb.h" -#include "pb/response_warn_history.pb.h" -#include "pb/response_warn_list.pb.h" -#include "pb/serverinfo_ban.pb.h" -#include "pb/serverinfo_chat_message.pb.h" -#include "pb/serverinfo_deckstorage.pb.h" -#include "pb/serverinfo_replay.pb.h" -#include "pb/serverinfo_user.pb.h" #include "servatrice.h" #include "servatrice_database_interface.h" #include "server_logger.h" -#include "server_player.h" -#include "server_response_containers.h" -#include "server_room.h" #include "settingscache.h" -#include "trice_limits.h" #include "version_string.h" #include #include #include +#include #include #include #include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +inline Q_LOGGING_CATEGORY(AbstractServerSocketInterfaceLog, "abstract_server_socket_interface"); +inline Q_LOGGING_CATEGORY(TcpServerSocketInterfaceLog, "tcp_server_socket_interface"); +inline Q_LOGGING_CATEGORY(WebsocketServerSocketInterfaceLog, "websocket_server_socket_interface"); + static const int protocolVersion = 14; AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server, @@ -126,7 +135,7 @@ bool AbstractServerSocketInterface::initSession() void AbstractServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socketError) { - qDebug() << "Socket error:" << socketError; + qCWarning(AbstractServerSocketInterfaceLog) << "Socket error:" << socketError; prepareDestroy(); } @@ -179,6 +188,10 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc); case SessionCommand::REPLAY_DELETE_MATCH: return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc); + case SessionCommand::REPLAY_GET_CODE: + return cmdReplayGetCode(cmd.GetExtension(Command_ReplayGetCode::ext), rc); + case SessionCommand::REPLAY_SUBMIT_CODE: + return cmdReplaySubmitCode(cmd.GetExtension(Command_ReplaySubmitCode::ext), rc); case SessionCommand::REGISTER: return cmdRegisterAccount(cmd.GetExtension(Command_Register::ext), rc); break; @@ -735,6 +748,135 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; } +/** + * Generates a hash for the given replay folder, used for auth when replay sharing. + * This is a separate function in case we change the hash implementation in the future. + * + * Currently, we append together the first 128 bytes of the first 3 replays in the game. + * Then we md5 hash it, base64 encode it, and truncate the result to 10 characters. + * + * @param gameId The replay match to hash + * @return The hash as a QString. Returns an empty string if failed + */ +QString AbstractServerSocketInterface::createHashForReplay(int gameId) +{ + QSqlQuery *query = + sqlInterface->prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3"); + query->bindValue(":id_game", gameId); + + if (!sqlInterface->execSqlQuery(query)) + return ""; + + QByteArray replaysBytes; + while (query->next()) { + QByteArray replay = query->value(0).toByteArray(); + replay.truncate(128); + replaysBytes.append(replay); + } + + auto hash = + QCryptographicHash::hash(replaysBytes, QCryptographicHash::Md5).toBase64(QByteArray::OmitTrailingEquals); + hash.truncate(10); + return hash; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Command_ReplayGetCode &cmd, + ResponseContainer &rc) +{ + if (authState != PasswordRight) + return Response::RespFunctionNotAllowed; + + // Check that user has access to replay match + { + QSqlQuery *query = sqlInterface->prepareQuery( + "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); + query->bindValue(":id_game", cmd.game_id()); + query->bindValue(":id_player", userInfo->id()); + if (!sqlInterface->execSqlQuery(query)) + return Response::RespInternalError; + if (!query->next()) + return Response::RespAccessDenied; + } + + QString hash = createHashForReplay(cmd.game_id()); + if (hash.isEmpty()) { + return Response::RespInternalError; + } + + // code is of the form - + QString code = QString(QString::number(cmd.game_id()) + "-" + hash); + + Response_ReplayGetCode *re = new Response_ReplayGetCode; + re->set_replay_code(code.toStdString()); + rc.setResponseExtension(re); + + return Response::RespOk; +} + +Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, + ResponseContainer & /*rc*/) +{ + // code is of the form - + QString code = QString::fromStdString(cmd.replay_code()); + QStringList split = code.split("-"); + if (split.size() != 2) { + // always return the same error response if code is incorrect, to not leak info to user + return Response::RespNameNotFound; + } + QString gameId = split[0]; + QString hash = split[1]; + + // Determine if the replay actually exists (and grab the replay name while at it) + auto *replayExistsQuery = + sqlInterface->prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1"); + replayExistsQuery->bindValue(":id_game", gameId); + if (!sqlInterface->execSqlQuery(replayExistsQuery)) { + return Response::RespInternalError; + } + if (!replayExistsQuery->next()) { + return Response::RespNameNotFound; + } + + const auto &replayName = replayExistsQuery->value(0).toString(); + + // Check if hash is correct + if (hash != createHashForReplay(gameId.toInt())) { + return Response::RespNameNotFound; + } + + // Determine if user already has access to replay + auto *alreadyAccessQuery = sqlInterface->prepareQuery( + "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); + alreadyAccessQuery->bindValue(":id_game", gameId); + alreadyAccessQuery->bindValue(":id_player", userInfo->id()); + if (!sqlInterface->execSqlQuery(alreadyAccessQuery)) { + return Response::RespInternalError; + } + if (alreadyAccessQuery->next()) { + return Response::RespOk; + } + + // Grant the User access to the replay + auto *grantReplayAccessQuery = + sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " + "values(:idgame, :idplayer, :replayname, 0)"); + grantReplayAccessQuery->bindValue(":idgame", gameId); + grantReplayAccessQuery->bindValue(":idplayer", userInfo->id()); + grantReplayAccessQuery->bindValue(":replayname", replayName); + + if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { + return Response::RespInternalError; + } + + // update user's view + Event_ReplayAdded event; + SessionEvent *se = prepareSessionEvent(event); + sendProtocolItem(*se); + delete se; + + return Response::RespOk; +} + // MODERATOR FUNCTIONS. // May be called by admins and moderators. Permission is checked by the calling function. Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd, @@ -835,11 +977,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnList(const Comma Response_WarnList *re = new Response_WarnList; QString officialWarnings = settingsCache->value("server/officialwarnings").toString(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList warningsList = officialWarnings.split(",", Qt::SkipEmptyParts); -#else - QStringList warningsList = officialWarnings.split(",", QString::SkipEmptyParts); -#endif for (const QString &warning : warningsList) { re->add_warning(warning.toStdString()); } @@ -967,7 +1105,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Com clientIdQuery->bindValue(":client_id", nameFromStdString(cmd.clientid())); sqlInterface->execSqlQuery(clientIdQuery); if (!sqlInterface->execSqlQuery(clientIdQuery)) { - qDebug("ClientID username ban lookup failed: SQL Error"); + qCWarning(AbstractServerSocketInterfaceLog) << "ClientID username ban lookup failed: SQL Error"; } else { while (clientIdQuery->next()) { userName = clientIdQuery->value(0).toString(); @@ -1019,7 +1157,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C { QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Got register command for user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Got register command for user:" << userName; bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); if (!registrationEnabled) { @@ -1035,13 +1173,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C const auto parsedEmailParts = EmailParser::parseEmailAddress(nameFromStdString(cmd.email())); const auto emailUser = parsedEmailParts.first; const auto emailDomain = parsedEmailParts.second; -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts); -#else - const QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts); - const QStringList emailWhiteListFilters = emailWhiteList.split(",", QString::SkipEmptyParts); -#endif bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); if (requireEmailForRegistration && emailUser.isEmpty()) { @@ -1070,7 +1203,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C return Response::RespEmailBlackListed; } - // TODO: Move this method outside of the db interface + //! \todo Move this method outside of the db interface QString errorString; if (!sqlInterface->usernameIsValid(userName, errorString)) { if (servatrice->getEnableRegistrationAudit()) @@ -1161,7 +1294,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C country, !requireEmailActivation); if (regSucceeded) { - qDebug() << "Accepted register command for user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted register command for user:" << userName; if (requireEmailActivation) { QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_activation_emails (name) values(:name)"); @@ -1193,7 +1326,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const C bool AbstractServerSocketInterface::tooManyRegistrationAttempts(const QString &ipAddress) { - // TODO: implement + //! \todo implement Q_UNUSED(ipAddress); return false; } @@ -1209,7 +1342,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const C clientId = "UNKNOWN"; if (sqlInterface->activateUser(userName, token)) { - qDebug() << "Accepted activation for user" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Accepted activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), @@ -1217,7 +1350,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const C return Response::RespActivationAccepted; } else { - qDebug() << "Failed activation for user" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Failed activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), @@ -1365,7 +1498,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(c const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password request from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password request from user:" << userName; if (!servatrice->getEnableForgotPassword()) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1447,7 +1580,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(con Q_UNUSED(rc); QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password reset from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password reset from user:" << userName; if (!sqlInterface->doesForgotPasswordExist(userName)) { if (servatrice->getEnableForgotPasswordAudit()) @@ -1498,7 +1631,7 @@ AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPa const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); - qDebug() << "Received reset password challenge from user:" << userName; + qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password challenge from user:" << userName; if (!servatrice->getEnableForgotPasswordChallenge()) { if (servatrice->getEnableForgotPasswordAudit()) { @@ -1793,13 +1926,8 @@ TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); -#else - connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, - SLOT(catchSocketError(QAbstractSocket::SocketError))); -#endif } TcpServerSocketInterface::~TcpServerSocketInterface() @@ -1848,13 +1976,16 @@ void TcpServerSocketInterface::flushOutputQueue() unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size + 4); - item.SerializeToArray(buf.data() + 4, size); - buf.data()[3] = (unsigned char)size; - buf.data()[2] = (unsigned char)(size >> 8); - buf.data()[1] = (unsigned char)(size >> 16); - buf.data()[0] = (unsigned char)(size >> 24); - // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. - writeToSocket(buf); + if (item.SerializeToArray(buf.data() + 4, size)) { + buf.data()[3] = (unsigned char)size; + buf.data()[2] = (unsigned char)(size >> 8); + buf.data()[1] = (unsigned char)(size >> 16); + buf.data()[0] = (unsigned char)(size >> 24); + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } totalBytes += size + 4; locker.relock(); @@ -1887,41 +2018,48 @@ void TcpServerSocketInterface::readClient() return; CommandContainer newCommandContainer; + bool ok; try { - newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); + ok = newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); } catch (std::exception &e) { - qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ << + qCWarning(TcpServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Exception:" << e.what(); - qDebug() << "Message coming from:" << getAddress(); - qDebug() << "Message length:" << messageLength; - qDebug() << "Message content:" << inputBuffer.toHex(); + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << messageLength << Qt::endl + << "Message content:" << inputBuffer.toHex(); } catch (...) { - qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ << + qCWarning(TcpServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Message coming from:" << getAddress(); + << Qt::endl + << "Message coming from:" << getAddress(); } - inputBuffer.remove(0, messageLength); messageInProgress = false; - // dirty hack to make v13 client display the correct error message - if (handshakeStarted) - processCommandContainer(newCommandContainer); - else if (!newCommandContainer.has_cmd_id()) { - handshakeStarted = true; - if (!initTcpSession()) - prepareDestroy(); + if (ok) { + // dirty hack to make v13 client display the correct error message + if (handshakeStarted) + processCommandContainer(newCommandContainer); + else if (!newCommandContainer.has_cmd_id()) { + handshakeStarted = true; + if (!initTcpSession()) + prepareDestroy(); + } + // end of hack + } else { + qCWarning(TcpServerSocketInterfaceLog) << "parsing error!"; } - // end of hack + } while (!inputBuffer.isEmpty()); } @@ -1972,10 +2110,8 @@ void WebsocketServerSocketInterface::initConnection(void *_socket) } socket = (QWebSocket *)_socket; socket->setParent(this); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) // https://bugreports.qt.io/browse/QTBUG-70693 socket->setMaxAllowedIncomingMessageSize(1500000); // 1.5MB -#endif address = socket->peerAddress(); @@ -2051,9 +2187,12 @@ void WebsocketServerSocketInterface::flushOutputQueue() unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size); - item.SerializeToArray(buf.data(), size); - // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. - writeToSocket(buf); + if (item.SerializeToArray(buf.data(), size)) { + // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. + writeToSocket(buf); + } else { + qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; + } totalBytes += size; locker.relock(); @@ -2069,30 +2208,37 @@ void WebsocketServerSocketInterface::binaryMessageReceived(const QByteArray &mes servatrice->incRxBytes(message.size()); CommandContainer newCommandContainer; + bool ok; try { - newCommandContainer.ParseFromArray(message.data(), message.size()); + ok = newCommandContainer.ParseFromArray(message.data(), message.size()); } catch (std::exception &e) { - qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ << + qCWarning(WebsocketServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Exception:" << e.what(); - qDebug() << "Message coming from:" << getAddress(); - qDebug() << "Message length:" << message.size(); - qDebug() << "Message content:" << message.toHex(); + << Qt::endl + << "Exception:" << e.what() << Qt::endl + << "Message coming from:" << getAddress() << Qt::endl + << "Message length:" << message.size() << Qt::endl + << "Message content:" << message.toHex(); } catch (...) { - qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ << + qCWarning(WebsocketServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio - __FUNCTION__; + __FUNCTION__ #else - __PRETTY_FUNCTION__; + __PRETTY_FUNCTION__ #endif - qDebug() << "Message coming from:" << getAddress(); + << Qt::endl + << "Message coming from:" << getAddress(); } - processCommandContainer(newCommandContainer); + if (ok) { + processCommandContainer(newCommandContainer); + } else { + qCWarning(WebsocketServerSocketInterfaceLog) << "parsing error!"; + } } bool AbstractServerSocketInterface::isPasswordLongEnough(const int passwordLength) diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 88d0fc549..e10aa0dde 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -20,12 +20,11 @@ #ifndef SERVERSOCKETINTERFACE_H #define SERVERSOCKETINTERFACE_H -#include "server_protocolhandler.h" - #include #include #include #include +#include class Servatrice; class Servatrice_DatabaseInterface; @@ -44,6 +43,8 @@ class Command_ReplayList; class Command_ReplayDownload; class Command_ReplayModifyMatch; class Command_ReplayDeleteMatch; +class Command_ReplayGetCode; +class Command_ReplaySubmitCode; class Command_BanFromServer; class Command_UpdateServerMessage; @@ -97,6 +98,9 @@ private: Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc); Response::ResponseCode cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer &rc); + QString createHashForReplay(int gameId); + Response::ResponseCode cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc); + Response::ResponseCode cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer &rc); Response::ResponseCode cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer &rc); Response::ResponseCode cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer &rc); Response::ResponseCode cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc); @@ -142,7 +146,9 @@ public: AbstractServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent = 0); - ~AbstractServerSocketInterface(){}; + ~AbstractServerSocketInterface() + { + } bool initSession(); virtual QHostAddress getPeerAddress() const = 0; @@ -171,7 +177,7 @@ public: QString getConnectionType() const { return "tcp"; - }; + } private: QTcpSocket *socket; @@ -184,11 +190,11 @@ protected: void writeToSocket(QByteArray &data) { socket->write(data); - }; + } void flushSocket() { socket->flush(); - }; + } void initSessionDeprecated(); bool initTcpSession(); protected slots: @@ -218,7 +224,7 @@ public: QString getConnectionType() const { return "websocket"; - }; + } private: QWebSocket *socket; @@ -228,11 +234,11 @@ protected: void writeToSocket(QByteArray &data) { socket->sendBinaryMessage(data); - }; + } void flushSocket() { socket->flush(); - }; + } bool initWebsocketSession(); protected slots: void binaryMessageReceived(const QByteArray &message); diff --git a/servatrice/src/settingscache.cpp b/servatrice/src/settingscache.cpp index 14bfaebfb..f6dcd5fc8 100644 --- a/servatrice/src/settingscache.cpp +++ b/servatrice/src/settingscache.cpp @@ -11,12 +11,7 @@ SettingsCache::SettingsCache(const QString &fileName, QSettings::Format format, // first, figure out if we are running in portable mode isPortableBuild = QFile::exists(qApp->applicationDirPath() + "/portable.dat"); - QStringList disallowedRegExpStr = -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts); -#else - value("users/disallowedregexp", "").toString().split(",", QString::SkipEmptyParts); -#endif + QStringList disallowedRegExpStr = value("users/disallowedregexp", "").toString().split(",", Qt::SkipEmptyParts); disallowedRegExpStr.removeDuplicates(); for (const QString ®ExpStr : disallowedRegExpStr) { disallowedRegExp.append(QRegularExpression(QString("\\A%1\\z").arg(regExpStr))); diff --git a/servatrice/src/signalhandler.h b/servatrice/src/signalhandler.h index af9f40cb8..bf8d9e52a 100644 --- a/servatrice/src/signalhandler.h +++ b/servatrice/src/signalhandler.h @@ -10,7 +10,9 @@ class SignalHandler : public QObject Q_OBJECT public: SignalHandler(QObject *parent = 0); - ~SignalHandler(){}; + ~SignalHandler() + { + } static void sigHupHandler(int /* sig */); static void sigSegvHandler(int sig); diff --git a/servatrice/src/smtp/qxtmailmessage.cpp b/servatrice/src/smtp/qxtmailmessage.cpp index ca003d875..ff376c1a9 100644 --- a/servatrice/src/smtp/qxtmailmessage.cpp +++ b/servatrice/src/smtp/qxtmailmessage.cpp @@ -27,9 +27,8 @@ * \class QxtMailMessage * \inmodule QxtNetwork * \brief The QxtMailMessage class encapsulates an e-mail according to RFC 2822 and related specifications - * TODO: {implicitshared} */ - +//! \todo {implicitshared} #include "qxtmailmessage.h" #include "qxtmail_p.h" diff --git a/servatrice/src/smtp/qxtsmtp.cpp b/servatrice/src/smtp/qxtsmtp.cpp index 951492d7a..6326b101d 100644 --- a/servatrice/src/smtp/qxtsmtp.cpp +++ b/servatrice/src/smtp/qxtsmtp.cpp @@ -334,11 +334,7 @@ void QxtSmtpPrivate::authenticate() state = Authenticated; emit qxt_p().authenticated(); } else { -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QStringList auth = extensions["AUTH"].toUpper().split(' ', Qt::SkipEmptyParts); -#else - QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts); -#endif if (auth.contains("CRAM-MD5")) { authCramMD5(); } else if (auth.contains("PLAIN")) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f6b2d12d2..fffaf1bda 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,16 +1,22 @@ +# NOTE: Qt modules for tests are defined centrally in cmake/FindQtRuntime.cmake (the _TEST_NEEDED variable). +# If a new test needs additional Qt modules, add them there — not in individual test CMakeLists.txt files. enable_testing() + add_test(NAME dummy_test COMMAND dummy_test) add_test(NAME expression_test COMMAND expression_test) - add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) +add_test(NAME deck_hash_performance_test COMMAND deck_hash_performance_test) +set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) + # Find GTest add_executable(dummy_test dummy_test.cpp) add_executable(expression_test expression_test.cpp) add_executable(test_age_formatting test_age_formatting.cpp) add_executable(password_hash_test password_hash_test.cpp) +add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) find_package(GTest) @@ -41,14 +47,23 @@ if(NOT GTEST_FOUND) add_dependencies(expression_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) + add_dependencies(deck_hash_performance_test gtest) endif() include_directories(${GTEST_INCLUDE_DIRS}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) -target_link_libraries(expression_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) target_link_libraries(test_age_formatting Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -target_link_libraries(password_hash_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + password_hash_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) +target_link_libraries( + deck_hash_performance_test libcockatrice_deck_list libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} +) +add_subdirectory(card_zone_algorithms) add_subdirectory(carddatabase) add_subdirectory(loading_from_clipboard) +add_subdirectory(movecard_tests) add_subdirectory(oracle) diff --git a/tests/card_zone_algorithms/CMakeLists.txt b/tests/card_zone_algorithms/CMakeLists.txt new file mode 100644 index 000000000..889e92eaa --- /dev/null +++ b/tests/card_zone_algorithms/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(card_zone_algorithms_test card_zone_algorithms_test.cpp) + +target_include_directories(card_zone_algorithms_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src/game/zones/logic) + +target_link_libraries( + card_zone_algorithms_test + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} +) + +add_test(NAME card_zone_algorithms_test COMMAND card_zone_algorithms_test) + +if(NOT GTEST_FOUND) + add_dependencies(card_zone_algorithms_test gtest) +endif() diff --git a/tests/card_zone_algorithms/card_zone_algorithms_test.cpp b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp new file mode 100644 index 000000000..cc098cae9 --- /dev/null +++ b/tests/card_zone_algorithms/card_zone_algorithms_test.cpp @@ -0,0 +1,159 @@ +#include "card_zone_algorithms.h" + +#include +#include + +struct MockCardRef +{ +}; + +struct MockCard +{ + int idSet = 0; + bool idWasCalled = false; + MockCardRef cardRefSet{}; + bool cardRefWasCalled = false; + bool resetStateCalled = false; + bool resetStateKeepAnnotations = false; + bool visibleSet = false; + + void setId(int id) + { + idSet = id; + idWasCalled = true; + } + + void setCardRef(MockCardRef ref) + { + cardRefSet = ref; + cardRefWasCalled = true; + } + + void resetState(bool keepAnnotations) + { + resetStateCalled = true; + resetStateKeepAnnotations = keepAnnotations; + } + + void setVisible(bool visible) + { + visibleSet = visible; + } +}; + +class MockCardList +{ + std::vector cards; + bool contentsKnown; + +public: + explicit MockCardList(bool _contentsKnown) : contentsKnown(_contentsKnown) + { + } + + int size() const + { + return static_cast(cards.size()); + } + + void insert(int index, MockCard *card) + { + cards.insert(cards.begin() + index, card); + } + + bool getContentsKnown() const + { + return contentsKnown; + } + + MockCard *at(int index) const + { + return cards.at(index); + } +}; + +class AddCardAlgorithmTest : public ::testing::Test +{ +protected: + MockCardList knownList{true}; + MockCardList unknownList{false}; +}; + +TEST_F(AddCardAlgorithmTest, NegativeIndexClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, -1, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, IndexBeyondSizeClampsToEnd) +{ + MockCard a, b; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 999, false); + + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &b); + EXPECT_EQ(knownList.size(), 2); +} + +TEST_F(AddCardAlgorithmTest, ContentsKnownPreservesIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_FALSE(card.idWasCalled); + EXPECT_FALSE(card.cardRefWasCalled); + EXPECT_TRUE(card.visibleSet); +} + +TEST_F(AddCardAlgorithmTest, ContentsUnknownClearsIdentity) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(unknownList, &card, 0, false); + + EXPECT_TRUE(card.idWasCalled); + EXPECT_EQ(card.idSet, -1); + EXPECT_TRUE(card.cardRefWasCalled); +} + +TEST_F(AddCardAlgorithmTest, MidListInsertionPreservesOrder) +{ + MockCard a, b, c; + CardZoneAlgorithms::addCardToList(knownList, &a, 0, false); + CardZoneAlgorithms::addCardToList(knownList, &b, 1, false); + CardZoneAlgorithms::addCardToList(knownList, &c, 1, false); + + EXPECT_EQ(knownList.size(), 3); + EXPECT_EQ(knownList.at(0), &a); + EXPECT_EQ(knownList.at(1), &c); + EXPECT_EQ(knownList.at(2), &b); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsFalsePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, false); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_FALSE(card.resetStateKeepAnnotations); +} + +TEST_F(AddCardAlgorithmTest, KeepAnnotationsTruePassedThrough) +{ + MockCard card; + CardZoneAlgorithms::addCardToList(knownList, &card, 0, true); + + EXPECT_TRUE(card.resetStateCalled); + EXPECT_TRUE(card.resetStateKeepAnnotations); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 4b82e2fb6..987e23cd5 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -1,58 +1,51 @@ +cmake_minimum_required(VERSION 3.16) +project(CardDatabaseTests VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + +# ------------------------ +# Definitions +# ------------------------ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VERSION_NAME}::Network - ${COCKATRICE_QT_VERSION_NAME}::Widgets ${COCKATRICE_QT_VERSION_NAME}::Svg -) +# ------------------------ +# Card Database Test +# ------------------------ +add_executable(carddatabase_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} carddatabase_test.cpp mocks.cpp) -if(Qt6_FOUND) - qt6_wrap_cpp( - MOCKS_SOURCES ../../cockatrice/src/settings/cache_settings.h ../../cockatrice/src/settings/card_database_settings.h - ) -elseif(Qt5_FOUND) - qt5_wrap_cpp( - MOCKS_SOURCES ../../cockatrice/src/settings/cache_settings.h ../../cockatrice/src/settings/card_database_settings.h - ) -endif() - -add_executable( +target_link_libraries( carddatabase_test - ${MOCKS_SOURCES} - ${VERSION_STRING_CPP} - ../../cockatrice/src/game/cards/card_database.cpp - ../../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp - ../../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp - ../../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp - ../../cockatrice/src/game/cards/card_info.cpp - ../../cockatrice/src/game/cards/exact_card.cpp - ../../cockatrice/src/settings/settings_manager.cpp - carddatabase_test.cpp - mocks.cpp + PRIVATE libcockatrice_card + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} ) -add_executable( - filter_string_test - ${MOCKS_SOURCES} - ${VERSION_STRING_CPP} - ../../cockatrice/src/game/cards/card_database.cpp - ../../cockatrice/src/game/cards/card_database_manager.cpp - ../../cockatrice/src/game/cards/card_database_parser/card_database_parser.cpp - ../../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_3.cpp - ../../cockatrice/src/game/cards/card_database_parser/cockatrice_xml_4.cpp - ../../cockatrice/src/game/cards/card_info.cpp - ../../cockatrice/src/game/cards/exact_card.cpp - ../../cockatrice/src/game/filters/filter_card.cpp - ../../cockatrice/src/game/filters/filter_string.cpp - ../../cockatrice/src/game/filters/filter_tree.cpp - ../../cockatrice/src/settings/settings_manager.cpp - filter_string_test.cpp - mocks.cpp -) -if(NOT GTEST_FOUND) - add_dependencies(carddatabase_test gtest) - add_dependencies(filter_string_test gtest) -endif() - -target_link_libraries(carddatabase_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -target_link_libraries(filter_string_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) add_test(NAME carddatabase_test COMMAND carddatabase_test) -add_test(NAME filter_string_test COMMAND filter_string_test) + +# ------------------------ +# Filter String Test +# (guard must match the condition for libcockatrice_filters in the root CMakeLists.txt) +# ------------------------ +if(WITH_ORACLE OR WITH_CLIENT) + add_executable(filter_string_test ${MOCKS_SOURCES} ${VERSION_STRING_CPP} filter_string_test.cpp mocks.cpp) + + target_link_libraries( + filter_string_test + PRIVATE libcockatrice_filters + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} + ) + + add_test(NAME filter_string_test COMMAND filter_string_test) + + if(NOT GTEST_FOUND) + add_dependencies(filter_string_test gtest) + endif() +endif() + +# ------------------------ +# Dependencies on gtest +# ------------------------ +if(NOT GTEST_FOUND) + add_dependencies(carddatabase_test gtest) +endif() diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 4818cd22e..3fa0e3834 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -1,33 +1,36 @@ #include "mocks.h" +#include "test_card_database_path_provider.h" #include "gtest/gtest.h" +#include +#include namespace { TEST(CardDatabaseTest, LoadXml) { - settingsCache = new SettingsCache; - CardDatabase *db = new CardDatabase; + CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), new TestCardDatabasePathProvider(), + new NoopCardSetPriorityController()); // ensure the card database is empty at start ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty at start"; ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty at start"; - ASSERT_EQ(0, db->getAllMainCardTypes().size()) << "Types not empty at start"; + ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty at start"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status at start"; // load dummy cards and test result db->loadCardDatabases(); - ASSERT_EQ(8, db->getCardList().size()) << "Wrong card count after load"; - ASSERT_EQ(4, db->getSetList().size()) << "Wrong sets count after load"; - ASSERT_EQ(3, db->getAllMainCardTypes().size()) << "Wrong types count after load"; + ASSERT_EQ(9, db->getCardList().size()) << "Wrong card count after load"; + ASSERT_EQ(5, db->getSetList().size()) << "Wrong sets count after load"; + ASSERT_EQ(3, db->query()->getAllMainCardTypes().size()) << "Wrong types count after load"; ASSERT_EQ(Ok, db->getLoadStatus()) << "Wrong status after load"; // ensure the card database is empty after clear() db->clear(); ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty after clear"; ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty after clear"; - ASSERT_EQ(0, db->getAllMainCardTypes().size()) << "Types not empty after clear"; + ASSERT_EQ(0, db->query()->getAllMainCardTypes().size()) << "Types not empty after clear"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; } } // namespace diff --git a/tests/carddatabase/data/cards.xml b/tests/carddatabase/data/cards.xml index f235ab4f9..2c1c09ed8 100644 --- a/tests/carddatabase/data/cards.xml +++ b/tests/carddatabase/data/cards.xml @@ -11,7 +11,7 @@ G 2G 2 - Creature + Creature — Cat Creature 3/3
@@ -26,7 +26,22 @@ R 2RR 4 - Creature + Creature — Dog + Creature + 4/4 +
+
+ + Doctor + WHO + 0 + Why did wizards introduce two-word creature types + + 222 + R + 2RR + 4 + Creature — Human Time Lord Doctor Creature 4/4 diff --git a/tests/carddatabase/filter_string_test.cpp b/tests/carddatabase/filter_string_test.cpp index b29660159..c6d68be1f 100644 --- a/tests/carddatabase/filter_string_test.cpp +++ b/tests/carddatabase/filter_string_test.cpp @@ -1,8 +1,10 @@ -#include "../../cockatrice/src/game/cards/card_database_manager.h" -#include "../../cockatrice/src/game/filters/filter_string.h" #include "mocks.h" +#include "test_card_database_path_provider.h" #include "gtest/gtest.h" +#include +#include +#include #define QUERY(name, card, query, match) \ TEST_F(CardQuery, name) \ @@ -18,15 +20,21 @@ class CardQuery : public ::testing::Test protected: void SetUp() override { - cat = CardDatabaseManager::getInstance()->getCardBySimpleName("Cat"); - notDeadAfterAll = CardDatabaseManager::getInstance()->getCardBySimpleName("Not Dead"); - truth = CardDatabaseManager::getInstance()->getCardBySimpleName("Truth"); + CardDatabase *db = new CardDatabase(nullptr, new NoopCardPreferenceProvider(), + new TestCardDatabasePathProvider(), new NoopCardSetPriorityController()); + db->loadCardDatabases(); + + cat = db->query()->getCardBySimpleName("Cat"); + notDeadAfterAll = db->query()->getCardBySimpleName("Not Dead"); + truth = db->query()->getCardBySimpleName("Truth"); + doctor = db->query()->getCardBySimpleName("Doctor"); } // void TearDown() override {} CardData cat; CardData notDeadAfterAll; CardData truth; + CardData doctor; }; QUERY(Empty, cat, "", true) @@ -34,6 +42,9 @@ QUERY(Typing, cat, "t", true) QUERY(NonMatchingType, cat, "t:kithkin", false) QUERY(MatchingType, cat, "t:creature", true) +QUERY(MatchingCreatureType, cat, "t:cat", true) +QUERY(PartialMatchingType, cat, "t:ca", false) +QUERY(MatchingMultiWordType, doctor, "t:\"Time Lord\"", true) QUERY(Not1, cat, "NOT t:kithkin", true) QUERY(Not2, cat, "NOT t:creature", false) QUERY(NonKeyword1, cat, "not t:kithkin", false) @@ -60,13 +71,12 @@ QUERY(Color2, cat, "c:gw", true) QUERY(Color3, cat, "c!g", true) QUERY(Color4, cat, "c!gw", false) +QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true) + } // namespace int main(int argc, char **argv) { - settingsCache = new SettingsCache; - CardDatabaseManager::getInstance()->loadCardDatabases(); - ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index 4c3a81322..5568ac84f 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -1,458 +1,6 @@ #include "mocks.h" -CardDatabaseSettings::CardDatabaseSettings(const QString &settingPath, QObject *parent) - : SettingsManager(settingPath + "cardDatabase.ini", parent) +void CardPictureLoader::clearPixmapCache(CardInfoPtr /* card */) { } -void CardDatabaseSettings::setSortKey(QString /* shortName */, unsigned int /* sortKey */) -{ -} -void CardDatabaseSettings::setEnabled(QString /* shortName */, bool /* enabled */) -{ -} -void CardDatabaseSettings::setIsKnown(QString /* shortName */, bool /* isknown */) -{ -} -unsigned int CardDatabaseSettings::getSortKey(QString /* shortName */) -{ - return 0; -}; -bool CardDatabaseSettings::isEnabled(QString /* shortName */) -{ - return true; -}; -bool CardDatabaseSettings::isKnown(QString /* shortName */) -{ - return true; -}; - -QString SettingsCache::getDataPath() -{ - return ""; -} -QString SettingsCache::getSettingsPath() -{ - return ""; -} -void SettingsCache::translateLegacySettings() -{ -} -QString SettingsCache::getSafeConfigPath(QString /* configEntry */, QString defaultPath) const -{ - return defaultPath; -} -QString SettingsCache::getSafeConfigFilePath(QString /* configEntry */, QString defaultPath) const -{ - return defaultPath; -} -SettingsCache::SettingsCache() - : settings{new QSettings("global.ini", QSettings::IniFormat, this)}, shortcutsSettings{nullptr}, - cardDatabaseSettings{new CardDatabaseSettings("", this)}, serversSettings{nullptr}, messageSettings{nullptr}, - gameFiltersSettings{nullptr}, layoutsSettings{nullptr}, downloadSettings{nullptr}, - cardDatabasePath{QString("%1/cards.xml").arg(CARDDB_DATADIR)}, - customCardDatabasePath{QString("%1/customsets/").arg(CARDDB_DATADIR)}, - spoilerDatabasePath{QString("%1/spoiler.xml").arg(CARDDB_DATADIR)}, - tokenDatabasePath{QString("%1/tokens.xml").arg(CARDDB_DATADIR)} -{ -} -void SettingsCache::setUseTearOffMenus(bool /* _useTearOffMenus */) -{ -} -void SettingsCache::setCardViewInitialRowsMax(int /* _cardViewInitialRowsMax */) -{ -} -void SettingsCache::setCardViewExpandedRowsMax(int /* value */) -{ -} -void SettingsCache::setCloseEmptyCardView(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setFocusCardViewSearchBar(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setKnownMissingFeatures(const QString & /* _knownMissingFeatures */) -{ -} -void SettingsCache::setCardInfoViewMode(const int /* _viewMode */) -{ -} -void SettingsCache::setHighlightWords(const QString & /* _highlightWords */) -{ -} -void SettingsCache::setMasterVolume(int /* _masterVolume */) -{ -} -void SettingsCache::setLeftJustified(const QT_STATE_CHANGED_T /* _leftJustified */) -{ -} -void SettingsCache::setCardScaling(const QT_STATE_CHANGED_T /* _scaleCards */) -{ -} -void SettingsCache::setStackCardOverlapPercent(const int /* _verticalCardOverlapPercent */) -{ -} -void SettingsCache::setShowMessagePopups(const QT_STATE_CHANGED_T /* _showMessagePopups */) -{ -} -void SettingsCache::setShowMentionPopups(const QT_STATE_CHANGED_T /* _showMentionPopus */) -{ -} -void SettingsCache::setRoomHistory(const QT_STATE_CHANGED_T /* _roomHistory */) -{ -} -void SettingsCache::setLang(const QString & /* _lang */) -{ -} -void SettingsCache::setShowTipsOnStartup(bool /* _showTipsOnStartup */) -{ -} -void SettingsCache::setSeenTips(const QList & /* _seenTips */) -{ -} -void SettingsCache::setDeckPath(const QString & /* _deckPath */) -{ -} -void SettingsCache::setFiltersPath(const QString & /*_filtersPath */) -{ -} -void SettingsCache::setReplaysPath(const QString & /* _replaysPath */) -{ -} -void SettingsCache::setThemesPath(const QString & /* _themesPath */) -{ -} -void SettingsCache::setPicsPath(const QString & /* _picsPath */) -{ -} -void SettingsCache::setCardDatabasePath(const QString & /* _cardDatabasePath */) -{ -} -void SettingsCache::setCustomCardDatabasePath(const QString & /* _customCardDatabasePath */) -{ -} -void SettingsCache::setSpoilerDatabasePath(const QString & /* _spoilerDatabasePath */) -{ -} -void SettingsCache::setTokenDatabasePath(const QString & /* _tokenDatabasePath */) -{ -} -void SettingsCache::setThemeName(const QString & /* _themeName */) -{ -} -void SettingsCache::setTabVisualDeckStorageOpen(bool /*value*/) -{ -} -void SettingsCache::setTabServerOpen(bool /*value*/) -{ -} -void SettingsCache::setTabAccountOpen(bool /*value*/) -{ -} -void SettingsCache::setTabDeckStorageOpen(bool /*value*/) -{ -} -void SettingsCache::setTabReplaysOpen(bool /*value*/) -{ -} -void SettingsCache::setTabAdminOpen(bool /*value*/) -{ -} -void SettingsCache::setTabLogOpen(bool /*value*/) -{ -} -void SettingsCache::setPicDownload(QT_STATE_CHANGED_T /* _picDownload */) -{ -} -void SettingsCache::setShowStatusBar(bool /* value */) -{ -} -void SettingsCache::setNotificationsEnabled(QT_STATE_CHANGED_T /* _notificationsEnabled */) -{ -} -void SettingsCache::setSpectatorNotificationsEnabled(QT_STATE_CHANGED_T /* _spectatorNotificationsEnabled */) -{ -} -void SettingsCache::setBuddyConnectNotificationsEnabled(QT_STATE_CHANGED_T /* _buddyConnectNotificationsEnabled */) -{ -} -void SettingsCache::setDoubleClickToPlay(QT_STATE_CHANGED_T /* _doubleClickToPlay */) -{ -} -void SettingsCache::setClickPlaysAllSelected(QT_STATE_CHANGED_T /* _clickPlaysAllSelected */) -{ -} -void SettingsCache::setPlayToStack(QT_STATE_CHANGED_T /* _playToStack */) -{ -} -void SettingsCache::setStartingHandSize(int /* _startingHandSize */) -{ -} -void SettingsCache::setAnnotateTokens(QT_STATE_CHANGED_T /* _annotateTokens */) -{ -} -void SettingsCache::setTabGameSplitterSizes(const QByteArray & /* _tabGameSplitterSizes */) -{ -} -void SettingsCache::setShowShortcuts(QT_STATE_CHANGED_T /* _showShortcuts */) -{ -} -void SettingsCache::setDisplayCardNames(QT_STATE_CHANGED_T /* _displayCardNames */) -{ -} -void SettingsCache::setOverrideAllCardArtWithPersonalPreference(QT_STATE_CHANGED_T /* _overrideAllCardArt */) -{ -} -void SettingsCache::setBumpSetsWithCardsInDeckToTop(QT_STATE_CHANGED_T /* _bumpSetsWithCardsInDeckToTop */) -{ -} -void SettingsCache::setPrintingSelectorSortOrder(int /* _printingSelectorSortOrder */) -{ -} -void SettingsCache::setPrintingSelectorCardSize(int /* _printingSelectorCardSize */) -{ -} -void SettingsCache::setIncludeRebalancedCards(bool /* _includeRebalancedCards */) -{ -} -void SettingsCache::setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T /* _navigationButtonsVisible */) -{ -} -void SettingsCache::setDeckEditorBannerCardComboBoxVisible( - QT_STATE_CHANGED_T /* _deckEditorBannerCardComboBoxVisible */) -{ -} -void SettingsCache::setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T /* _deckEditorTagsWidgetVisible */) -{ -} -void SettingsCache::setVisualDeckStorageSortingOrder(int /* _visualDeckStorageSortingOrder */) -{ -} -void SettingsCache::setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T /* _showTags */) -{ -} -void SettingsCache::setVisualDeckStorageDefaultTagsList(QStringList /* _defaultTagsList */) -{ -} -void SettingsCache::setVisualDeckStorageSearchFolderNames(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setVisualDeckStorageShowBannerCardComboBox(QT_STATE_CHANGED_T /* _showBannerCardComboBox */) -{ -} -void SettingsCache::setVisualDeckStorageShowTagsOnDeckPreviews(QT_STATE_CHANGED_T /* _showTags */) -{ -} -void SettingsCache::setVisualDeckStorageCardSize(int /* _visualDeckStorageCardSize */) -{ -} -void SettingsCache::setVisualDeckStorageDrawUnusedColorIdentities( - QT_STATE_CHANGED_T /* _visualDeckStorageDrawUnusedColorIdentities */) -{ -} -void SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity( - int /* _visualDeckStorageUnusedColorIdentitiesOpacity */) -{ -} -void SettingsCache::setVisualDeckStorageTooltipType(int /* value */) -{ -} -void SettingsCache::setVisualDeckStoragePromptForConversion(bool /* _visualDeckStoragePromptForConversion */) -{ -} -void SettingsCache::setVisualDeckStorageAlwaysConvert(bool /* _visualDeckStorageAlwaysConvert */) -{ -} -void SettingsCache::setVisualDeckStorageInGame(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setVisualDeckStorageSelectionAnimation(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setDefaultDeckEditorType(int /* value */) -{ -} -void SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsEnabled(QT_STATE_CHANGED_T /* _enabled */) -{ -} -void SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsAmount(int /* _amount */) -{ -} -void SettingsCache::setVisualDeckEditorSampleHandSize(int /* _amount */) -{ -} -void SettingsCache::setHorizontalHand(QT_STATE_CHANGED_T /* _horizontalHand */) -{ -} -void SettingsCache::setInvertVerticalCoordinate(QT_STATE_CHANGED_T /* _invertVerticalCoordinate */) -{ -} -void SettingsCache::setMinPlayersForMultiColumnLayout(int /* _minPlayersForMultiColumnLayout */) -{ -} -void SettingsCache::setTapAnimation(QT_STATE_CHANGED_T /* _tapAnimation */) -{ -} -void SettingsCache::setAutoRotateSidewaysLayoutCards(QT_STATE_CHANGED_T /* _autoRotateSidewaysLayoutCards */) -{ -} -void SettingsCache::setOpenDeckInNewTab(QT_STATE_CHANGED_T /* _openDeckInNewTab */) -{ -} -void SettingsCache::setRewindBufferingMs(int /* _rewindBufferingMs */) -{ -} -void SettingsCache::setChatMention(QT_STATE_CHANGED_T /* _chatMention */) -{ -} -void SettingsCache::setChatMentionCompleter(const QT_STATE_CHANGED_T /* _enableMentionCompleter */) -{ -} -void SettingsCache::setChatMentionForeground(QT_STATE_CHANGED_T /* _chatMentionForeground */) -{ -} -void SettingsCache::setChatHighlightForeground(QT_STATE_CHANGED_T /* _chatHighlightForeground */) -{ -} -void SettingsCache::setChatMentionColor(const QString & /* _chatMentionColor */) -{ -} -void SettingsCache::setChatHighlightColor(const QString & /* _chatHighlightColor */) -{ -} -void SettingsCache::setZoneViewGroupByIndex(int /* _zoneViewGroupByIndex */) -{ -} -void SettingsCache::setZoneViewSortByIndex(int /* _zoneViewSortByIndex */) -{ -} -void SettingsCache::setZoneViewPileView(QT_STATE_CHANGED_T /* _zoneViewPileView */) -{ -} -void SettingsCache::setSoundEnabled(QT_STATE_CHANGED_T /* _soundEnabled */) -{ -} -void SettingsCache::setSoundThemeName(const QString & /* _soundThemeName */) -{ -} -void SettingsCache::setIgnoreUnregisteredUsers(QT_STATE_CHANGED_T /* _ignoreUnregisteredUsers */) -{ -} -void SettingsCache::setIgnoreUnregisteredUserMessages(QT_STATE_CHANGED_T /* _ignoreUnregisteredUserMessages */) -{ -} -void SettingsCache::setMainWindowGeometry(const QByteArray & /* _mainWindowGeometry */) -{ -} -void SettingsCache::setTokenDialogGeometry(const QByteArray & /* _tokenDialogGeometry */) -{ -} -void SettingsCache::setSetsDialogGeometry(const QByteArray & /* _setsDialogGeometry */) -{ -} -void SettingsCache::setPixmapCacheSize(const int /* _pixmapCacheSize */) -{ -} -void SettingsCache::setNetworkCacheSizeInMB(const int /* _networkCacheSize */) -{ -} -void SettingsCache::setNetworkRedirectCacheTtl(const int /* _redirectCacheTtl */) -{ -} -void SettingsCache::setClientID(const QString & /* _clientID */) -{ -} -void SettingsCache::setClientVersion(const QString & /* _clientVersion */) -{ -} -QStringList SettingsCache::getCountries() const -{ - static QStringList countries = QStringList() << "us"; - return countries; -} -void SettingsCache::setGameDescription(const QString /* _gameDescription */) -{ -} -void SettingsCache::setMaxPlayers(const int /* _maxPlayers */) -{ -} -void SettingsCache::setGameTypes(const QString /* _gameTypes */) -{ -} -void SettingsCache::setOnlyBuddies(const bool /* _onlyBuddies */) -{ -} -void SettingsCache::setOnlyRegistered(const bool /* _onlyRegistered */) -{ -} -void SettingsCache::setSpectatorsAllowed(const bool /* _spectatorsAllowed */) -{ -} -void SettingsCache::setSpectatorsNeedPassword(const bool /* _spectatorsNeedPassword */) -{ -} -void SettingsCache::setSpectatorsCanTalk(const bool /* _spectatorsCanTalk */) -{ -} -void SettingsCache::setSpectatorsCanSeeEverything(const bool /* _spectatorsCanSeeEverything */) -{ -} -void SettingsCache::setCreateGameAsSpectator(const bool /* _createGameAsSpectator */) -{ -} -void SettingsCache::setDefaultStartingLifeTotal(const int /* _startingLifeTotal */) -{ -} -void SettingsCache::setShareDecklistsOnLoad(const bool /* _shareDecklistsOnLoad */) -{ -} -void SettingsCache::setRememberGameSettings(const bool /* _rememberGameSettings */) -{ -} -void SettingsCache::setCheckUpdatesOnStartup(QT_STATE_CHANGED_T /* value */) -{ -} -void SettingsCache::setStartupCardUpdateCheckPromptForUpdate(bool /* value */) -{ -} -void SettingsCache::setStartupCardUpdateCheckAlwaysUpdate(bool /* value */) -{ -} -void SettingsCache::setCardUpdateCheckInterval(int /* value */) -{ -} -void SettingsCache::setLastCardUpdateCheck(QDate /* value */) -{ -} -void SettingsCache::setNotifyAboutUpdate(QT_STATE_CHANGED_T /* _notifyaboutupdate */) -{ -} -void SettingsCache::setNotifyAboutNewVersion(QT_STATE_CHANGED_T /* _notifyaboutnewversion */) -{ -} -void SettingsCache::setDownloadSpoilerStatus(bool /* _spoilerStatus */) -{ -} -void SettingsCache::setUpdateReleaseChannelIndex(int /* value */) -{ -} -void SettingsCache::setMaxFontSize(int /* _max */) -{ -} -void SettingsCache::setRoundCardCorners(bool /* _roundCardCorners */) -{ -} - -void PictureLoader::clearPixmapCache(CardInfoPtr /* card */) -{ -} - -SettingsCache *settingsCache; - -SettingsCache &SettingsCache::instance() -{ - return *settingsCache; -} diff --git a/tests/carddatabase/mocks.h b/tests/carddatabase/mocks.h index 23210810a..016642005 100644 --- a/tests/carddatabase/mocks.h +++ b/tests/carddatabase/mocks.h @@ -5,18 +5,11 @@ * with mocked objects. */ -#include -#include - #define PICTURELOADER_H -#include "../../cockatrice/src/game/cards/card_database.h" -#include "../../cockatrice/src/settings/cache_settings.h" -#include "../../cockatrice/src/utility/macros.h" +#include -extern SettingsCache *settingsCache; - -class PictureLoader +class CardPictureLoader { public: static void clearPixmapCache(CardInfoPtr card); diff --git a/tests/carddatabase/test_card_database_path_provider.h b/tests/carddatabase/test_card_database_path_provider.h new file mode 100644 index 000000000..8fc8ef962 --- /dev/null +++ b/tests/carddatabase/test_card_database_path_provider.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H +#define COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H + +#include + +class TestCardDatabasePathProvider : public ICardDatabasePathProvider +{ + +public: + QString getCardDatabasePath() const override + { + return QString("%1/cards.xml").arg(CARDDB_DATADIR); + } + QString getCustomCardDatabasePath() const override + { + return QString("%1/customsets/").arg(CARDDB_DATADIR); + } + QString getTokenDatabasePath() const override + { + return QString("%1/tokens.xml").arg(CARDDB_DATADIR); + } + QString getSpoilerCardDatabasePath() const override + { + return QString("%1/spoiler.xml").arg(CARDDB_DATADIR); + } +}; + +#endif // COCKATRICE_TEST_CARD_DATABASE_PATH_PROVIDER_H diff --git a/tests/deck_hash_performance_test.cpp b/tests/deck_hash_performance_test.cpp new file mode 100644 index 000000000..154283e8e --- /dev/null +++ b/tests/deck_hash_performance_test.cpp @@ -0,0 +1,81 @@ +#include "gtest/gtest.h" +#include +#include + +static constexpr int amount = 1e5; +QString repeatDeck; +QString numberDeck; +QString uniquesDeck; +QString uniquesXorDeck; +QString duplicatesDeck; + +TEST(DeckHashTest, RepeatTest) +{ + DeckList decklist(repeatDeck); + for (int i = 0; i < amount; ++i) { + decklist.getDeckHash(); + decklist.refreshDeckHash(); + } + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "5cac19qm") << "The hash does not match!"; +} + +TEST(DeckHashTest, NumberTest) +{ + DeckList decklist(numberDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "e0m38p19") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTest) +{ + DeckList decklist(uniquesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "88prk025") << "The hash does not match!"; +} + +TEST(DeckHashTest, UniquesTestXor) +{ + DeckList decklist(uniquesXorDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "hkn6q4pf") << "The hash does not match!"; +} + +TEST(DeckHashTest, DuplicatesTest) +{ + DeckList decklist(duplicatesDeck); + auto hash = decklist.getDeckHash().toStdString(); + ASSERT_EQ(hash, "ekt6tg1h") << "The hash does not match!"; +} + +int main(int argc, char **argv) +{ + const QString deckStart = + R"()"; + const QString deckEnd = R"()"; + + repeatDeck = + deckStart + + R"()" + + deckEnd; + numberDeck = deckStart + QString(R"()").arg(amount) + deckEnd; + + QStringList deckString{deckStart}; + QStringList deckStringXor = deckString; + int len = QString::number(amount).length(); + for (int i = 0; i < amount; ++i) { + // creates already sorted list + deckString << R"()"; + // xor in order to mess with sorting + deckStringXor << R"()"; + } + deckString << deckEnd; + deckStringXor << deckEnd; + uniquesDeck = deckString.join(""); + uniquesXorDeck = deckStringXor.join(""); + + duplicatesDeck = deckStart + QString(R"()").repeated(amount) + deckEnd; + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/expression_test.cpp b/tests/expression_test.cpp index 51c4039a4..4fbe98cb9 100644 --- a/tests/expression_test.cpp +++ b/tests/expression_test.cpp @@ -1,7 +1,6 @@ -#include "../common/expression.h" - #include "gtest/gtest.h" #include +#include #define TEST_EXPR(name, a, b) \ TEST(ExpressionTest, name) \ diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index 7b635a12d..719d62f45 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -1,17 +1,12 @@ add_definitions("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") -add_executable( - loading_from_clipboard_test ../../common/decklist.cpp clipboard_testing.cpp loading_from_clipboard_test.cpp -) +add_executable(loading_from_clipboard_test ${VERSION_STRING_CPP} clipboard_testing.cpp loading_from_clipboard_test.cpp) if(NOT GTEST_FOUND) add_dependencies(loading_from_clipboard_test gtest) endif() -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VERSION_NAME}::Network - ${COCKATRICE_QT_VERSION_NAME}::Widgets -) - target_link_libraries( - loading_from_clipboard_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} + loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES} + ${TEST_QT_MODULES} ) add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp index b2021bde5..a9977b800 100644 --- a/tests/loading_from_clipboard/clipboard_testing.cpp +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -1,23 +1,35 @@ #include "clipboard_testing.h" #include +#include +#include + +DeckList getDeckList(const QString &clipboard) +{ + DeckList deckList; + QString cp(clipboard); + QTextStream stream(&cp); // text stream requires local copy + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); + return deckList; +} void testEmpty(const QString &clipboard) { - QString cp(clipboard); - DeckList deckList; - QTextStream stream(&cp); // text stream requires local copy - deckList.loadFromStream_Plain(stream, false); + DeckList deckList = getDeckList(clipboard); ASSERT_TRUE(deckList.getCardList().isEmpty()); } +void testHash(const QString &clipboard, const std::string &hash) +{ + DeckList deckList = getDeckList(clipboard); + + ASSERT_EQ(deckList.getDeckHash().toStdString(), hash); +} + void testDeck(const QString &clipboard, const Result &result) { - QString cp(clipboard); - DeckList deckList; - QTextStream stream(&cp); // text stream requires local copy - deckList.loadFromStream_Plain(stream, false); + DeckList deckList = getDeckList(clipboard); ASSERT_EQ(result.name, deckList.getName().toStdString()); ASSERT_EQ(result.comments, deckList.getComments().toStdString()); diff --git a/tests/loading_from_clipboard/clipboard_testing.h b/tests/loading_from_clipboard/clipboard_testing.h index 00d73b636..41d8ea794 100644 --- a/tests/loading_from_clipboard/clipboard_testing.h +++ b/tests/loading_from_clipboard/clipboard_testing.h @@ -1,9 +1,8 @@ #ifndef CLIPBOARD_TESTING_H #define CLIPBOARD_TESTING_H -#include "../../common/decklist.h" - #include "gtest/gtest.h" +#include // using std types because qt types aren't understood by gtest (without this you'll get less nice errors) using CardRows = QVector>; @@ -22,7 +21,7 @@ struct Result }; void testEmpty(const QString &clipboard); - +void testHash(const QString &clipboard, const std::string &hash); void testDeck(const QString &clipboard, const Result &result); #endif // CLIPBOARD_TESTING_H diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp index 6f9762be9..fcfbb22db 100644 --- a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -203,6 +203,22 @@ TEST(LoadingFromClipboardTest, emptyMainBoard) testEmpty(clipboard); } +TEST(LoadingFromClipboardTest, emptyHash) +{ + QString clipboard(""); + + testHash(clipboard, "r8sq7riu"); +} + +TEST(LoadingFromClipboardTest, deckHash) +{ + QString clipboard("1 Mountain\n" + "2 Island\n" + "SB: 3 Forest\n"); + + testHash(clipboard, "5cac19qm"); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/movecard_tests/CMakeLists.txt b/tests/movecard_tests/CMakeLists.txt new file mode 100755 index 000000000..769047148 --- /dev/null +++ b/tests/movecard_tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(reverse_card_move_test reverse_card_move_test.cpp) + +if(NOT GTEST_FOUND) + add_dependencies(reverse_card_move_test gtest) +endif() + +target_link_libraries( + reverse_card_move_test + PRIVATE libcockatrice_network_server_remote + PRIVATE libcockatrice_rng + PRIVATE Threads::Threads + PRIVATE ${GTEST_BOTH_LIBRARIES} + PRIVATE ${TEST_QT_MODULES} +) + +add_test(NAME reverse_card_move_test COMMAND reverse_card_move_test) diff --git a/tests/movecard_tests/reverse_card_move_test.cpp b/tests/movecard_tests/reverse_card_move_test.cpp new file mode 100644 index 000000000..2231a7e3b --- /dev/null +++ b/tests/movecard_tests/reverse_card_move_test.cpp @@ -0,0 +1,91 @@ +#include "game/server_abstract_player.h" +#include "game/server_card.h" +#include "game/server_cardzone.h" +#include "game/server_game.h" +#include "server_response_containers.h" +#include "server_room.h" +#include "server_test_helpers.h" + +#include +#include +#include +#include +#include + +RNG_Abstract *rng = nullptr; // this needs to be defined due to other functions in server + +TEST(ReverseCardMoveTest, MoveCardFromBottomTest) +{ + ServerInfo_User user; + user.set_name("test-user"); + + // instantiate a fake server instance + FakeServer server; + Server_Room room(0, 0, "", "", "", "", false, "", {}, &server); + Server_Game game(user, 1, "", "", 2, QList(), false, false, false, false, false, false, 20, false, &room); + Server_AbstractPlayer player(&game, 1, user, false, nullptr); + Server_CardZone deckZone(&player, ZoneNames::DECK, true, ServerInfo_Zone::PublicZone); + Server_CardZone exileZone(&player, ZoneNames::EXILE, true, ServerInfo_Zone::PublicZone); + + // setup the deck with 20 useless cards + for (int i = 0; i < 20; i++) { + auto *cardUseless = new Server_Card({"Card Useless", "card-Useless"}, player.newCardId(), i, 0); + deckZone.insertCard(cardUseless, i, 0); + } + + // add 4 cards to the end of it + auto *cardA = new Server_Card({"Card A", "card-a"}, player.newCardId(), 20, 0); + auto *cardB = new Server_Card({"Card B", "card-b"}, player.newCardId(), 21, 0); + auto *cardC = new Server_Card({"Card C", "card-c"}, player.newCardId(), 22, 0); + auto *cardD = new Server_Card({"Card D", "card-d"}, player.newCardId(), 23, 0); + + deckZone.insertCard(cardA, 20, 0); + deckZone.insertCard(cardB, 21, 0); + deckZone.insertCard(cardC, 22, 0); + deckZone.insertCard(cardD, 23, 0); + + // try to move them, with the expected client given order (n-3, n-2, n-1, n) + CardToMove moveA; + moveA.set_card_id(cardA->getId()); + CardToMove moveB; + moveB.set_card_id(cardB->getId()); + CardToMove moveC; + moveC.set_card_id(cardC->getId()); + CardToMove moveD; + moveD.set_card_id(cardD->getId()); + + QList cardsToMove = {&moveA, &moveB, &moveC, &moveD}; + GameEventStorage ges; + + const auto response = player.moveCard(ges, &deckZone, cardsToMove, &exileZone, 0, 0, false, false, false); + + EXPECT_EQ(response, Response::RespOk); + + int positionA; + int positionB; + int positionC; + int positionD; + // find the cards in the destination zone and check they are the right card + EXPECT_EQ(exileZone.getCard(cardA->getId(), &positionA), cardA); + EXPECT_EQ(exileZone.getCard(cardB->getId(), &positionB), cardB); + EXPECT_EQ(exileZone.getCard(cardC->getId(), &positionC), cardC); + EXPECT_EQ(exileZone.getCard(cardD->getId(), &positionD), cardD); + + // check that they are at the expected index + EXPECT_EQ(cardA->getX(), 3); + EXPECT_EQ(cardB->getX(), 2); + EXPECT_EQ(cardC->getX(), 1); + EXPECT_EQ(cardD->getX(), 0); + + // also check if the given positions are correct + EXPECT_EQ(positionA, 3); + EXPECT_EQ(positionB, 2); + EXPECT_EQ(positionC, 1); + EXPECT_EQ(positionD, 0); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/movecard_tests/server_test_helpers.h b/tests/movecard_tests/server_test_helpers.h new file mode 100644 index 000000000..fd2ed6c17 --- /dev/null +++ b/tests/movecard_tests/server_test_helpers.h @@ -0,0 +1,42 @@ +#include "server.h" +#include "server_database_interface.h" + +class MockDatabaseInterface : public Server_DatabaseInterface +{ +public: + AuthenticationResult checkUserPassword(Server_ProtocolHandler *, + const QString &, + const QString &, + const QString &, + QString &, + int &, + bool) override + { + return NotLoggedIn; + } + ServerInfo_User getUserData(const QString &, bool) override + { + return ServerInfo_User(); + } + int getNextGameId() override + { + return 1; + } + int getNextReplayId() override + { + return 1; + } + int getActiveUserCount(QString) override + { + return 1; + } +}; + +class FakeServer : public Server +{ +public: + FakeServer() + { + setDatabaseInterface(new MockDatabaseInterface()); + } +}; diff --git a/tests/oracle/CMakeLists.txt b/tests/oracle/CMakeLists.txt index 0c11eb751..c5c1e9097 100644 --- a/tests/oracle/CMakeLists.txt +++ b/tests/oracle/CMakeLists.txt @@ -4,8 +4,6 @@ if(NOT GTEST_FOUND) add_dependencies(parse_cipt_test gtest) endif() -set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Widgets) - -target_link_libraries(parse_cipt_test cockatrice_common Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries(parse_cipt_test Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) add_test(NAME parse_cipt_test COMMAND parse_cipt_test) diff --git a/tests/password_hash_test.cpp b/tests/password_hash_test.cpp index 84e7c8eb1..38d9b6315 100644 --- a/tests/password_hash_test.cpp +++ b/tests/password_hash_test.cpp @@ -1,8 +1,7 @@ -#include "../common/passwordhasher.h" -#include "../common/rng_abstract.h" -#include "../common/rng_sfmt.h" - #include "gtest/gtest.h" +#include +#include +#include RNG_Abstract *rng; diff --git a/tests/test_age_formatting.cpp b/tests/test_age_formatting.cpp index 4593a7b56..e4fc64cf9 100644 --- a/tests/test_age_formatting.cpp +++ b/tests/test_age_formatting.cpp @@ -1,4 +1,4 @@ -#include "../cockatrice/src/server/user/user_info_box.h" +#include "../cockatrice/src/interface/widgets/server/user/user_info_box.h" #include "gtest/gtest.h" diff --git a/vcpkg b/vcpkg index c63619856..74e653621 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit c63619856b89f0af4d77ba2049396039e4985418 +Subproject commit 74e6536215718009aae747d86d84b78376bf9e09 diff --git a/webclient/package-lock.json b/webclient/package-lock.json index b5d961a75..6b12b4ad6 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -4866,9 +4866,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/express": { "version": "4.17.14", @@ -5641,9 +5641,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": { "acorn": "bin/acorn" }, @@ -5671,6 +5671,17 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -6344,6 +6355,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6481,9 +6500,9 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -6499,10 +6518,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -6609,9 +6629,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -7849,9 +7869,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==" + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" }, "node_modules/emittery": { "version": "0.8.1", @@ -7886,12 +7906,12 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -8002,9 +8022,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==" }, "node_modules/es-shim-unscopables": { "version": "1.0.0", @@ -14383,11 +14403,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -14815,9 +14839,9 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -18747,11 +18771,15 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/temp-dir": { @@ -18823,9 +18851,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -19208,9 +19236,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -19367,9 +19395,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -19395,34 +19423,35 @@ } }, "node_modules/webpack": { - "version": "5.99.9", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", - "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -19670,9 +19699,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "engines": { "node": ">=10.13.0" } @@ -19709,9 +19738,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -23591,9 +23620,9 @@ } }, "@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "@types/express": { "version": "4.17.14", @@ -24245,9 +24274,9 @@ } }, "acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" }, "acorn-globals": { "version": "6.0.0", @@ -24265,6 +24294,11 @@ } } }, + "acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==" + }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -24752,6 +24786,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -24869,14 +24908,15 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" }, "browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "requires": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" } }, "bser": { @@ -24950,9 +24990,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==" + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -25843,9 +25883,9 @@ } }, "electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==" + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==" }, "emittery": { "version": "0.8.1", @@ -25868,12 +25908,12 @@ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "requires": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" } }, "entities": { @@ -25963,9 +26003,9 @@ } }, "es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==" }, "es-shim-unscopables": { "version": "1.0.0", @@ -30636,9 +30676,9 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==" }, "loader-utils": { "version": "2.0.4", @@ -30953,9 +30993,9 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" }, "normalize-path": { "version": "3.0.0", @@ -33582,9 +33622,9 @@ } }, "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==" }, "temp-dir": { "version": "2.0.0", @@ -33637,9 +33677,9 @@ } }, "terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "requires": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -33910,9 +33950,9 @@ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, "update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "requires": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -34023,9 +34063,9 @@ } }, "watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -34045,34 +34085,35 @@ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" }, "webpack": { - "version": "5.99.9", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", - "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "requires": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "dependencies": { "ajv": { @@ -34100,9 +34141,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "requires": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -34265,9 +34306,9 @@ } }, "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==" }, "websocket-driver": { "version": "0.7.4", diff --git a/webclient/prebuild.js b/webclient/prebuild.js index f931f508b..3b420f32c 100644 --- a/webclient/prebuild.js +++ b/webclient/prebuild.js @@ -12,7 +12,7 @@ const serverPropsFile = `${ROOT_DIR}/server-props.json`; const masterProtoFile = `${ROOT_DIR}/proto-files.json`; const sharedFiles = [ - ['../common/pb', protoFilesDir], + ['../libcockatrice_protocol/libcockatrice/protocol/pb', protoFilesDir], ['../cockatrice/resources/countries', `${ROOT_DIR}/images/countries`], ]; diff --git a/webclient/public/locales/fi/translation.json b/webclient/public/locales/fi/translation.json index 57fb2e1a9..12737731a 100644 --- a/webclient/public/locales/fi/translation.json +++ b/webclient/public/locales/fi/translation.json @@ -354,7 +354,7 @@ "label": { "autoConnect": "Auto Connect", "forgot": "Forgot Password", - "login": "Login", + "login": "Kirjaudu sisään", "savePassword": "Save Password", "savedPassword": "Saved Password" } @@ -376,7 +376,7 @@ "ResetPasswordForm": { "error": "Password reset failed", "label": { - "reset": "Reset Password" + "reset": "Nollaa Salasana" } } } \ No newline at end of file diff --git a/webclient/public/locales/ru/translation.json b/webclient/public/locales/ru/translation.json index 457a772e2..5e92f790e 100644 --- a/webclient/public/locales/ru/translation.json +++ b/webclient/public/locales/ru/translation.json @@ -24,263 +24,263 @@ "required": "Необходимо" }, "countries": { - "AD": "Andorra", - "AE": "United Arab Emirates", - "AF": "Afghanistan", - "AG": "Antigua and Barbuda", - "AI": "Anguilla", - "AL": "Albania", - "AM": "Armenia", - "AO": "Angola", - "AQ": "Antarctica", - "AR": "Argentina", - "AS": "American Samoa", - "AT": "Austria", - "AU": "Australia", - "AW": "Aruba", - "AX": "Åland Islands", - "AZ": "Azerbaijan", - "BA": "Bosnia and Herzegovina", - "BB": "Barbados", - "BD": "Bangladesh", - "BE": "Belgium", - "BF": "Burkina Faso", - "BG": "Bulgaria", - "BH": "Bahrain", - "BI": "Burundi", - "BJ": "Benin", - "BL": "Saint Barthélemy", - "BM": "Bermuda", - "BN": "Brunei Darussalam", - "BO": "Bolivia", - "BQ": "Bonaire, Sint Eustatius and Saba", - "BR": "Brazil", - "BS": "Bahamas", - "BT": "Bhutan", - "BV": "Bouvet Island", - "BW": "Botswana", - "BY": "Belarus", - "BZ": "Belize", - "CA": "Canada", - "CC": "Cocos (Keeling) Islands", - "CD": "DR Congo", - "CF": "Central African Republic", - "CG": "Republic of the Congo", - "CH": "Switzerland", - "CI": "Ivory Coast", - "CK": "Cook Islands", - "CL": "Chile", - "CM": "Cameroon", - "CN": "China", - "CO": "Colombia", - "CR": "Costa Rica", - "CU": "Cuba", - "CV": "Cape Verde", - "CW": "Curaçao", - "CX": "Christmas Island", - "CY": "Cyprus", - "CZ": "Czechia", - "DE": "Germany", - "DJ": "Djibouti", - "DK": "Denmark", - "DM": "Dominica", - "DO": "Dominican Republic", - "DZ": "Algeria", - "EC": "Ecuador", - "EE": "Estonia", - "EG": "Egypt", - "EH": "Western Sahara", - "ER": "Eritrea", - "ES": "Spain", - "ET": "Ethiopia", - "FI": "Finland", - "FJ": "Fiji", - "FK": "Falkland Islands", - "FM": "Micronesia", - "FO": "Faroe Islands", - "FR": "France", - "GA": "Gabon", - "GB": "United Kingdom", - "GD": "Grenada", - "GE": "Georgia", - "GF": "French Guiana", - "GG": "Guernsey", - "GH": "Ghana", - "GI": "Gibraltar", - "GL": "Greenland", - "GM": "Gambia", - "GN": "Guinea", - "GP": "Guadeloupe", - "GQ": "Equatorial Guinea", - "GR": "Greece", - "GS": "South Georgia and the South Sandwich Islands", - "GT": "Guatemala", - "GU": "Guam", - "GW": "Guinea-Bissau", - "GY": "Guyana", - "HK": "Hong Kong", - "HM": "Heard Island and McDonald Islands", - "HN": "Honduras", - "HR": "Croatia", - "HT": "Haiti", - "HU": "Hungary", - "ID": "Indonesia", - "IE": "Ireland", - "IL": "Israel", - "IM": "Isle of Man", - "IN": "India", - "IO": "British Indian Ocean Territory", - "IQ": "Iraq", - "IR": "Iran", - "IS": "Iceland", - "IT": "Italy", - "JE": "Jersey", - "JM": "Jamaica", - "JO": "Jordan", - "JP": "Japan", - "KE": "Kenya", - "KG": "Kyrgyzstan", - "KH": "Cambodia", - "KI": "Kiribati", - "KM": "Comoros", - "KN": "Saint Kitts and Nevis", - "KP": "North Korea", - "KR": "South Korea", - "KW": "Kuwait", - "KY": "Cayman Islands", - "KZ": "Kazakhstan", - "LA": "Laos", - "LB": "Lebanon", - "LC": "Saint Lucia", - "LI": "Liechtenstein", - "LK": "Sri Lanka", - "LR": "Liberia", - "LS": "Lesotho", - "LT": "Lithuania", - "LU": "Luxembourg", - "LV": "Latvia", - "LY": "Libya", - "MA": "Morocco", - "MC": "Monaco", - "MD": "Moldova", - "ME": "Montenegro", - "MF": "Saint Martin (French part)", - "MG": "Madagascar", - "MH": "Marshall Islands", - "MK": "North Macedonia", - "ML": "Mali", - "MM": "Myanmar", - "MN": "Mongolia", - "MO": "Macao", - "MP": "Northern Mariana Islands", - "MQ": "Martinique", - "MR": "Mauritania", - "MS": "Montserrat", - "MT": "Malta", - "MU": "Mauritius", - "MV": "Maldives", - "MW": "Malawi", - "MX": "Mexico", - "MY": "Malaysia", - "MZ": "Mozambique", - "NA": "Namibia", - "NC": "New Caledonia", - "NE": "Niger", - "NF": "Norfolk Island", - "NG": "Nigeria", - "NI": "Nicaragua", - "NL": "Netherlands", - "NO": "Norway", - "NP": "Nepal", - "NR": "Nauru", - "NU": "Niue", - "NZ": "New Zealand", - "OM": "Oman", - "PA": "Panama", - "PE": "Peru", - "PF": "French Polynesia", - "PG": "Papua New Guinea", - "PH": "Philippines", - "PK": "Pakistan", - "PL": "Poland", - "PM": "Saint Pierre and Miquelon", - "PN": "Pitcairn", - "PR": "Puerto Rico", - "PS": "Palestine", - "PT": "Portugal", - "PW": "Palau", - "PY": "Paraguay", - "QA": "Qatar", - "RE": "Réunion", - "RO": "Romania", - "RS": "Serbia", - "RU": "Russia", - "RW": "Rwanda", - "SA": "Saudi Arabia", - "SB": "Solomon Islands", - "SC": "Seychelles", - "SD": "Sudan", - "SE": "Sweden", - "SG": "Singapore", - "SH": "Saint Helena, Ascension and Tristan da Cunha", - "SI": "Slovenia", - "SJ": "Svalbard and Jan Mayen", - "SK": "Slovakia", - "SL": "Sierra Leone", - "SM": "San Marino", - "SN": "Senegal", - "SO": "Somalia", - "SR": "Suriname", - "SS": "South Sudan", - "ST": "Sao Tome and Principe", - "SV": "El Salvador", - "SX": "Sint Maarten (Dutch part)", - "SY": "Syria", - "SZ": "Eswatini", - "TC": "Turks and Caicos Islands", - "TD": "Chad", - "TF": "TAAF", - "TG": "Togo", - "TH": "Thailand", - "TJ": "Tajikistan", - "TK": "Tokelau", - "TL": "Timor-Leste", - "TM": "Turkmenistan", - "TN": "Tunisia", - "TO": "Tonga", - "TR": "Turkey", - "TT": "Trinidad and Tobago", - "TV": "Tuvalu", - "TW": "Taiwan", - "TZ": "Tanzania", - "UA": "Ukraine", - "UG": "Uganda", - "UM": "United States Minor Outlying Islands", - "US": "United States", - "UY": "Uruguay", - "UZ": "Uzbekistan", - "VA": "Holy See", - "VC": "Saint Vincent and the Grenadines", - "VE": "Venezuela", - "VG": "British Virgin Islands", - "VI": "U.S. Virgin Islands", - "VN": "Viet Nam", - "VU": "Vanuatu", - "WF": "Wallis and Futuna", - "WS": "Samoa", - "YE": "Yemen", - "YT": "Mayotte", - "XK": "Kosovo", - "ZA": "South Africa", - "ZM": "Zambia", - "ZW": "Zimbabwe", - "EU": "European Union" + "AD": "Андорра", + "AE": "ОАЭ", + "AF": "Афганистан", + "AG": "Антигуа и Барбуда", + "AI": "Ангилья", + "AL": "Албания", + "AM": "Армения", + "AO": "Ангола", + "AQ": "Антарктика", + "AR": "Аргентина", + "AS": "Американское Самоа", + "AT": "Австрия", + "AU": "Австралия", + "AW": "Аруба", + "AX": "Аландские острова", + "AZ": "Азербайджан", + "BA": "Босния и Герцеговина", + "BB": "Барбадос", + "BD": "Бангладеш", + "BE": "Бельгия", + "BF": "Буркина Фасо", + "BG": "Болгария", + "BH": "Бахрейн", + "BI": "Бурунди", + "BJ": "Бенин", + "BL": "Сен-Бартелеми", + "BM": "Бермуды", + "BN": "Бруней", + "BO": "Боливия", + "BQ": "Бонэйр", + "BR": "Бразилия", + "BS": "Багамы", + "BT": "Бутан", + "BV": "Остров Буве", + "BW": "Ботсвана", + "BY": "Беларусь", + "BZ": "Белиз", + "CA": "Канада", + "CC": "Кокосовые острова", + "CD": "ДР Конго", + "CF": "ЦАР", + "CG": "Республика Конго", + "CH": "Швейцария", + "CI": "Кот-д'Ивуар", + "CK": "Острова Кука", + "CL": "Чили", + "CM": "Камерун", + "CN": "Китай", + "CO": "Колумбия", + "CR": "Коста-Рика", + "CU": "Куба", + "CV": "Кабо-Верде", + "CW": "Кюрасао", + "CX": "Остров Рождества", + "CY": "Кипр", + "CZ": "Чехия", + "DE": "Германия", + "DJ": "Джибути", + "DK": "Дания", + "DM": "Доминика", + "DO": "Доминиканская Республика", + "DZ": "Алжир", + "EC": "Эквадор", + "EE": "Эстония", + "EG": "Египет", + "EH": "Западная Сахара", + "ER": "Эритрея", + "ES": "Испания", + "ET": "Эфиопия", + "FI": "Финляндия", + "FJ": "Фиджи", + "FK": "Фолклендские острова", + "FM": "Микронезия", + "FO": "Фарерские о-ва", + "FR": "Франция", + "GA": "Габон", + "GB": "Великобритания", + "GD": "Гренада", + "GE": "Грузия", + "GF": "Французская Гвиана", + "GG": "Гернси", + "GH": "Гана", + "GI": "Гибралтар", + "GL": "Гренландия", + "GM": "Гамбия", + "GN": "Гвинея", + "GP": "Гваделупа", + "GQ": "Экваториальная Гвинея", + "GR": "Греция", + "GS": "Южная Георгия и Южные Сандвичевы острова", + "GT": "Гватемала", + "GU": "Гуам", + "GW": "Гвинея-Бисау", + "GY": "Гайана", + "HK": "Гонконг", + "HM": "Остров Херд и остров Макдональд", + "HN": "Гондурас", + "HR": "Хорватия", + "HT": "Гаити", + "HU": "Венгрия", + "ID": "Индонезия", + "IE": "Ирландия", + "IL": "Израиль", + "IM": "о-в Мэн", + "IN": "Индия", + "IO": "Британская территория в Индийском океане ", + "IQ": "Ирак", + "IR": "Иран", + "IS": "Исландия", + "IT": "Италия", + "JE": "Джерси", + "JM": "Ямайка", + "JO": "Иордания", + "JP": "Япония", + "KE": "Кения", + "KG": "Киргизия", + "KH": "Камбоджия", + "KI": "Кирибати", + "KM": "Коморы", + "KN": "Сент-Китс и Невис", + "KP": "Северная Корея", + "KR": "Южная Корея", + "KW": "Кувейт", + "KY": "Каймановы острова", + "KZ": "Казахстан", + "LA": "Лаос", + "LB": "Ливан", + "LC": "Сент-Люсия", + "LI": "Лихтенштейн", + "LK": "Шри-Ланка", + "LR": "Либерия", + "LS": "Лесото", + "LT": "Литва", + "LU": "Люксембург", + "LV": "Латвия", + "LY": "Ливия", + "MA": "Морокко", + "MC": "Монако", + "MD": "Молдавия", + "ME": "Черногория", + "MF": "Сен-Мартен (владение Франции)", + "MG": "Мадагарскар", + "MH": "Маршалловы о-ва", + "MK": "Северная Македония", + "ML": "Мали", + "MM": "Мьянма (Бирма)", + "MN": "Монголия", + "MO": "Макао", + "MP": "Северные Марианские о-ва", + "MQ": "Мартиника", + "MR": "Мавритания", + "MS": "Монтсеррат", + "MT": "Мальта", + "MU": "о. Маврикий", + "MV": "Мальдивы", + "MW": "Малави", + "MX": "Мексика", + "MY": "Малазия", + "MZ": "Мозамбик", + "NA": "Намибия", + "NC": "Новая Каледония", + "NE": "Нигер", + "NF": "Остров Норфолк", + "NG": "Нигерия", + "NI": "Никарагуа", + "NL": "Нидерланды", + "NO": "Норвегия", + "NP": "Непал", + "NR": "Науру", + "NU": "Ниуэ", + "NZ": "Новая Зеландия", + "OM": "Оман", + "PA": "Панама", + "PE": "Перу", + "PF": "Французская Полинезия", + "PG": "Папуа-Новая Гвинея", + "PH": "Филиппины", + "PK": "Пакистан", + "PL": "Польша", + "PM": "Сен-Пьер и Микелон", + "PN": "о-ва Питкэрн", + "PR": "Пуэрто-Рико", + "PS": "Палестина", + "PT": "Португалия", + "PW": "Палау", + "PY": "Парагва", + "QA": "Катар", + "RE": "Реюньон", + "RO": "Румыния", + "RS": "Сербия", + "RU": "Российская Федерация", + "RW": "Руанда", + "SA": "Саудовская Аравия", + "SB": "Соломоновы о-ва", + "SC": "Сейшелы", + "SD": "Судан", + "SE": "Швеция", + "SG": "Сингапур", + "SH": "о. Св. Елены", + "SI": "Словения", + "SJ": "Шпицберген и Ян-Майен", + "SK": "Словакия", + "SL": "Сьерра-Леоне", + "SM": "Сан-Марино", + "SN": "Сенегал", + "SO": "Сомали", + "SR": "Суринам", + "SS": "Южный Судан", + "ST": "Сан-Томе и Принсипи", + "SV": "Сальвадор", + "SX": "Синт-Мартен (Дания)", + "SY": "Сирия", + "SZ": "Эсватини", + "TC": "Острова Теркс и Кайкос", + "TD": "Чад", + "TF": "Французские Южные и Антарктические территории", + "TG": "Того", + "TH": "Тайланд", + "TJ": "Таджикистан", + "TK": "Токелау", + "TL": "Восточный Тимор", + "TM": "Туркмения", + "TN": "Тунис", + "TO": "Тонго", + "TR": "Турция", + "TT": "Тринидад и Тобаго", + "TV": "Тувалу", + "TW": "Тайвань", + "TZ": "Танзания", + "UA": "Украина", + "UG": "Уганда", + "UM": "Внешние малые о-ва (США)", + "US": "США", + "UY": "Уругвай", + "UZ": "Узбекистан", + "VA": "Святой Престол", + "VC": "Сент-Винсент и Гренадины", + "VE": "Венесуэла", + "VG": "Британские Виргинские острова", + "VI": "Американские Виргинские острова", + "VN": "Вьетнам", + "VU": "Вануату", + "WF": "о-ва Уоллис и Футуна", + "WS": "Самоа", + "YE": "Йемен", + "YT": "Майотта", + "XK": "Косово", + "ZA": "ЮАР", + "ZM": "Замбия", + "ZW": "Зимбабве", + "EU": "Европейский Союз" }, "languages": { - "en-US": "English - US", - "fr": "French", - "nl": "Dutch", - "pt_BR": "Portuguese - Brazil" + "en-US": "Английский - США", + "fr": "Французский", + "nl": "Датский", + "pt_BR": "Португальский - Бразилия" } }, "KnownHosts": { @@ -295,36 +295,36 @@ "LoginContainer": { "header": { "title": "Логин", - "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + "subtitle": "Кросс-платформенный виртуальный стол для мультиплеерных ККИ" }, "footer": { - "registerPrompt": "Not registered yet?", - "registerAction": "Create an account", - "credit": "Cockatrice is an open source project", - "version": "Version" + "registerPrompt": "Ещё не зарегистрированы?", + "registerAction": "Создать учетную запись", + "credit": "Cockatrice - проект с открытым исходным кодом", + "version": "Версия" }, "content": { - "subtitle1": "Play multiplayer card games online.", - "subtitle2": "Cross-platform virtual tabletop for multiplayer card games. Forever free." + "subtitle1": "Играйте в мультиплеерные ККИ онлайн", + "subtitle2": "Кросс-платформенный виртуальный стол для мультиплеерных ККИ. Всегда будет бесплатным." }, "toasts": { - "passwordResetSuccessToast": "Password Reset Successfully", - "accountActivationSuccess": "Account Activated Successfully" + "passwordResetSuccessToast": "Пароль сброшен успешно", + "accountActivationSuccess": "Учетная запись активирована успешно" } }, "UnsupportedContainer": { - "title": "Unsupported Browser", - "subtitle1": "Please update your browser and/or check your permissions.", - "subtitle2": "Note: Private browsing causes some browsers to disable certain permissions or features." + "title": "Браузер не поддерживается", + "subtitle1": "Обновите бразуер и/или проверьте права доступа", + "subtitle2": "Обратите внимание, что использование анонимных браузеров может привести к неработоспособности некоторых фич и проблемами с правами доступа" }, "AccountActivationDialog": { - "title": "Account Activation", - "subtitle1": "Your account has not been activated yet.", - "subtitle2": "You need to provide the activation token received in the activation email." + "title": "Активация учетной записи", + "subtitle1": "Ваша учетная запись не активирована", + "subtitle2": "Ваш аккаунт пока не активирован. Вам необходимо следовать указаниям по активации, отправленным на ваш email" }, "KnownHostDialog": { - "title": "{mode, select, edit {Edit} other {Add}} Known Host", - "subtitle": "Adding a new host allows you to connect to different servers. Enter the details below to your host list." + "title": "{модифицировать, выбрать, редактировать {Edit} другой {Add}} известный хост", + "subtitle": "Добавление нового хоста позволит вам присоединяться к различным серверам. Введите данные о сервере в ваш список хостов" }, "RegistrationDialog": { "title": "Создать новый аккаунт" diff --git a/webclient/public/locales/tok/translation.json b/webclient/public/locales/tok/translation.json index b42f7b1c9..46437f558 100644 --- a/webclient/public/locales/tok/translation.json +++ b/webclient/public/locales/tok/translation.json @@ -333,7 +333,7 @@ "title": "Request Password Reset" }, "ResetPasswordDialog": { - "title": "Reset Password" + "title": "mi o sin e nimi ken" }, "AccountActivationForm": { "error": { @@ -354,7 +354,7 @@ "label": { "autoConnect": "Auto Connect", "forgot": "nimi ken pi kama sina li weka tan sona", - "login": "Login", + "login": "mi o kama e sina lon kulupu", "savePassword": "mi o awen e nimi ken", "savedPassword": "mi awen e nimi ken" } diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx index 368390708..7b3a46988 100644 --- a/webclient/src/api/AuthenticationService.tsx +++ b/webclient/src/api/AuthenticationService.tsx @@ -1,5 +1,6 @@ import { StatusEnum, User, WebSocketConnectReason, WebSocketConnectOptions } from 'types'; import { SessionCommands, webClient } from 'websocket'; +import { ProtoController } from 'websocket/services/ProtoController'; export class AuthenticationService { static login(options: WebSocketConnectOptions): void { @@ -39,7 +40,7 @@ export class AuthenticationService { } static isModerator(user: User): boolean { - const moderatorLevel = webClient.protobuf.controller.ServerInfo_User.UserLevelFlag.IsModerator; + const moderatorLevel = ProtoController.root.ServerInfo_User.UserLevelFlag.IsModerator; // @TODO tell cockatrice not to do this so shittily return (user.userLevel & moderatorLevel) === moderatorLevel; } diff --git a/webclient/src/api/ModeratorService.tsx b/webclient/src/api/ModeratorService.tsx index e7c8822e8..6c22ee55e 100644 --- a/webclient/src/api/ModeratorService.tsx +++ b/webclient/src/api/ModeratorService.tsx @@ -23,7 +23,7 @@ export class ModeratorService { ModeratorCommands.viewLogHistory(filters); } - static warnUser(userName: string, reason: string, clientid?: string, removeMessage?: boolean): void { - ModeratorCommands.warnUser(userName, reason, clientid, removeMessage); + static warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { + ModeratorCommands.warnUser(userName, reason, clientid, removeMessages); } } diff --git a/webclient/src/api/SessionService.tsx b/webclient/src/api/SessionService.tsx index 051954f93..2787f098d 100644 --- a/webclient/src/api/SessionService.tsx +++ b/webclient/src/api/SessionService.tsx @@ -1,6 +1,4 @@ import { SessionCommands } from 'websocket'; -import { common } from 'protobufjs'; -import IBytesValue = common.IBytesValue; export class SessionService { static addToBuddyList(userName: string) { @@ -27,7 +25,7 @@ export class SessionService { SessionCommands.accountEdit(passwordCheck, realName, email, country); } - static changeAccountImage(image: IBytesValue): void { + static changeAccountImage(image: Uint8Array): void { SessionCommands.accountImage(image); } diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index ed1f06977..8af5adb75 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -1,4 +1,4 @@ -import { WebSocketConnectOptions } from 'types'; +import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; import { Types } from './server.types'; export const Actions = { @@ -210,4 +210,33 @@ export const Actions = { type: Types.WARN_USER, userName, }), + grantReplayAccess: (replayId: number, moderatorName: string) => ({ + type: Types.GRANT_REPLAY_ACCESS, + replayId, + moderatorName, + }), + forceActivateUser: (usernameToActivate: string, moderatorName: string) => ({ + type: Types.FORCE_ACTIVATE_USER, + usernameToActivate, + moderatorName, + }), + getAdminNotes: (userName: string, notes: string) => ({ + type: Types.GET_ADMIN_NOTES, + userName, + notes, + }), + updateAdminNotes: (userName: string, notes: string) => ({ + type: Types.UPDATE_ADMIN_NOTES, + userName, + notes, + }), + replayList: (matchList: ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }), + replayAdded: (matchInfo: ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }), + replayModifyMatch: (gameId: number, doNotHide: boolean) => ({ type: Types.REPLAY_MODIFY_MATCH, gameId, doNotHide }), + replayDeleteMatch: (gameId: number) => ({ type: Types.REPLAY_DELETE_MATCH, gameId }), + backendDecks: (deckList: DeckList) => ({ type: Types.BACKEND_DECKS, deckList }), + deckNewDir: (path: string, dirName: string) => ({ type: Types.DECK_NEW_DIR, path, dirName }), + deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }), + deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }), + deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }), } diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 3a958043a..3d3b6d360 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -1,7 +1,7 @@ import { reset } from 'redux-form'; import { Actions } from './server.actions'; import { store } from 'store'; -import { WebSocketConnectOptions } from 'types'; +import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; export const Dispatch = { initialized: () => { @@ -177,4 +177,43 @@ export const Dispatch = { warnUser: (userName) => { store.dispatch(Actions.warnUser(userName)) }, + grantReplayAccess: (replayId: number, moderatorName: string) => { + store.dispatch(Actions.grantReplayAccess(replayId, moderatorName)); + }, + forceActivateUser: (usernameToActivate: string, moderatorName: string) => { + store.dispatch(Actions.forceActivateUser(usernameToActivate, moderatorName)); + }, + getAdminNotes: (userName: string, notes: string) => { + store.dispatch(Actions.getAdminNotes(userName, notes)); + }, + updateAdminNotes: (userName: string, notes: string) => { + store.dispatch(Actions.updateAdminNotes(userName, notes)); + }, + replayList: (matchList: ReplayMatch[]) => { + store.dispatch(Actions.replayList(matchList)); + }, + replayAdded: (matchInfo: ReplayMatch) => { + store.dispatch(Actions.replayAdded(matchInfo)); + }, + replayModifyMatch: (gameId: number, doNotHide: boolean) => { + store.dispatch(Actions.replayModifyMatch(gameId, doNotHide)); + }, + replayDeleteMatch: (gameId: number) => { + store.dispatch(Actions.replayDeleteMatch(gameId)); + }, + backendDecks: (deckList: DeckList) => { + store.dispatch(Actions.backendDecks(deckList)); + }, + deckNewDir: (path: string, dirName: string) => { + store.dispatch(Actions.deckNewDir(path, dirName)); + }, + deckDelDir: (path: string) => { + store.dispatch(Actions.deckDelDir(path)); + }, + deckUpload: (path: string, treeItem: DeckStorageTreeItem) => { + store.dispatch(Actions.deckUpload(path, treeItem)); + }, + deckDelete: (deckId: number) => { + store.dispatch(Actions.deckDelete(deckId)); + }, } diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index 499197852..97e9d99da 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -1,4 +1,6 @@ -import { WarnHistoryItem, BanHistoryItem, LogItem, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem } from 'types'; +import { + WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem +} from 'types'; import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; export interface ServerConnectParams { @@ -67,6 +69,9 @@ export interface ServerState { }; warnListOptions: WarnListItem[]; warnUser: string; + adminNotes: { [userName: string]: string }; + replays: ReplayMatch[]; + backendDecks: DeckList | null; } export interface ServerStateStatus { diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index 46daa4007..a63418eeb 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -1,10 +1,60 @@ -import { SortDirection, StatusEnum, UserLevelFlag, UserSortField } from 'types'; +import { DeckStorageFolder, DeckStorageTreeItem, SortDirection, StatusEnum, UserLevelFlag, UserSortField } from 'types'; import { SortUtil } from '../common'; import { ServerState } from './server.interfaces' import { Types } from './server.types'; +function splitPath(path: string): string[] { + return path ? path.split('/') : []; +} + +function insertAtPath(folder: DeckStorageFolder, pathSegments: string[], item: DeckStorageTreeItem): DeckStorageFolder { + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { + return { items: [...folder.items, item] }; + } + const [head, ...tail] = pathSegments; + const match = folder.items.find(child => child.name === head && child.folder); + if (match) { + return { + items: folder.items.map(child => + child === match + ? { ...child, folder: insertAtPath(child.folder!, tail, item) } + : child + ), + }; + } + const created: DeckStorageTreeItem = { id: 0, name: head, file: null, folder: insertAtPath({ items: [] }, tail, item) }; + return { items: [...folder.items, created] }; +} + +function removeById(folder: DeckStorageFolder, id: number): DeckStorageFolder { + return { + items: folder.items + .filter(item => item.id !== id) + .map(item => + item.folder ? { ...item, folder: removeById(item.folder, id) } : item + ), + }; +} + +function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckStorageFolder { + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { + return folder; + } + const [head, ...tail] = pathSegments; + if (tail.length === 0) { + return { items: folder.items.filter(item => !(item.name === head && item.folder !== null)) }; + } + return { + items: folder.items.map(item => + item.name === head && item.folder + ? { ...item, folder: removeByPath(item.folder, tail) } + : item + ), + }; +} + const initialState: ServerState = { initialized: false, buddyList: [], @@ -40,6 +90,9 @@ const initialState: ServerState = { warnHistory: {}, warnListOptions: [], warnUser: '', + adminNotes: {}, + replays: [], + backendDecks: null, }; export const serverReducer = (state = initialState, action: any) => { @@ -247,7 +300,7 @@ export const serverReducer = (state = initialState, action: any) => { messages: { ...state.messages, [userName]: [ - ...state.messages[userName], + ...(state.messages[userName] ?? []), action.messageData, ], } @@ -328,6 +381,17 @@ export const serverReducer = (state = initialState, action: any) => { warnUser: userName, }; } + case Types.GET_ADMIN_NOTES: + case Types.UPDATE_ADMIN_NOTES: { + const { userName, notes } = action; + return { + ...state, + adminNotes: { + ...state.adminNotes, + [userName]: notes, + } + }; + } case Types.ADJUST_MOD: { const { userName, shouldBeMod, shouldBeJudge } = action; @@ -346,6 +410,71 @@ export const serverReducer = (state = initialState, action: any) => { }) }; } + case Types.REPLAY_LIST: { + return { ...state, replays: [...action.matchList] }; + } + case Types.REPLAY_ADDED: { + return { ...state, replays: [...state.replays, action.matchInfo] }; + } + case Types.REPLAY_MODIFY_MATCH: { + return { + ...state, + replays: state.replays.map(r => + r.gameId === action.gameId ? { ...r, doNotHide: action.doNotHide } : r + ), + }; + } + case Types.REPLAY_DELETE_MATCH: { + return { ...state, replays: state.replays.filter(r => r.gameId !== action.gameId) }; + } + case Types.BACKEND_DECKS: { + return { ...state, backendDecks: action.deckList }; + } + case Types.DECK_UPLOAD: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem), + }, + }; + } + case Types.DECK_DELETE: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: removeById(state.backendDecks.root, action.deckId), + }, + }; + } + case Types.DECK_NEW_DIR: { + if (!state.backendDecks) { + return state; + } + const newFolder: DeckStorageTreeItem = { id: 0, name: action.dirName, file: null, folder: { items: [] } }; + return { + ...state, + backendDecks: { + root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder), + }, + }; + } + case Types.DECK_DEL_DIR: { + if (!state.backendDecks) { + return state; + } + return { + ...state, + backendDecks: { + root: removeByPath(state.backendDecks.root, splitPath(action.path)), + }, + }; + } default: return state; } diff --git a/webclient/src/store/server/server.selectors.ts b/webclient/src/store/server/server.selectors.ts index 263946425..fa9f82297 100644 --- a/webclient/src/store/server/server.selectors.ts +++ b/webclient/src/store/server/server.selectors.ts @@ -16,5 +16,7 @@ export const Selectors = { getUsers: ({ server }: State) => server.users, getLogs: ({ server }: State) => server.logs, getBuddyList: ({ server }: State) => server.buddyList, - getIgnoreList: ({ server }: State) => server.ignoreList + getIgnoreList: ({ server }: State) => server.ignoreList, + getReplays: ({ server }: State) => server.replays, + getBackendDecks: ({ server }: State) => server.backendDecks, } diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index ab9307295..fb4249011 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -54,4 +54,19 @@ export const Types = { WARN_HISTORY: '[Server] Warn History', WARN_LIST_OPTIONS: '[Server] Warn List Options', WARN_USER: '[Server] Warn User', + GRANT_REPLAY_ACCESS: '[Server] Grant Replay Access', + FORCE_ACTIVATE_USER: '[Server] Force Activate User', + GET_ADMIN_NOTES: '[Server] Get Admin Notes', + UPDATE_ADMIN_NOTES: '[Server] Update Admin Notes', + // Replay + REPLAY_LIST: '[Server] Replay List', + REPLAY_ADDED: '[Server] Replay Added', + REPLAY_MODIFY_MATCH: '[Server] Replay Modify Match', + REPLAY_DELETE_MATCH: '[Server] Replay Delete Match', + // Deck Storage + BACKEND_DECKS: '[Server] Backend Decks', + DECK_NEW_DIR: '[Server] Deck New Dir', + DECK_DEL_DIR: '[Server] Deck Del Dir', + DECK_UPLOAD: '[Server] Deck Upload', + DECK_DELETE: '[Server] Deck Delete', }; diff --git a/webclient/src/types/deckList.ts b/webclient/src/types/deckList.ts index 18212079e..9d7d792a6 100644 --- a/webclient/src/types/deckList.ts +++ b/webclient/src/types/deckList.ts @@ -13,6 +13,6 @@ export interface DeckStorageFile { export interface DeckStorageTreeItem { id: number; name: string; - file: DeckStorageFile; - folder: DeckStorageFolder; + file: DeckStorageFile | null; + folder: DeckStorageFolder | null; } diff --git a/webclient/src/types/index.ts b/webclient/src/types/index.ts index 00838a16d..ded9962e5 100644 --- a/webclient/src/types/index.ts +++ b/webclient/src/types/index.ts @@ -16,3 +16,4 @@ export * from './logs'; export * from './session'; export * from './deckList'; export * from './moderator'; +export * from './replay'; diff --git a/webclient/src/types/logs.ts b/webclient/src/types/logs.ts index 4c2f0364e..3cf34b486 100644 --- a/webclient/src/types/logs.ts +++ b/webclient/src/types/logs.ts @@ -1,8 +1,10 @@ export interface LogFilters { - userName: string; - ipAddress: string; - gameName: string; - gameId: string; - message: string; - logLocation: string; + userName?: string; + ipAddress?: string; + gameName?: string; + gameId?: string; + message?: string; + logLocation?: string[]; + dateRange: number; + maximumResults?: number; } diff --git a/webclient/src/types/replay.ts b/webclient/src/types/replay.ts new file mode 100644 index 000000000..dfa78538a --- /dev/null +++ b/webclient/src/types/replay.ts @@ -0,0 +1,16 @@ +export interface Replay { + replayId: number; + replayName: string; + duration: number; +} + +export interface ReplayMatch { + replayList: Replay[]; + gameId: number; + roomName: string; + timeStarted: number; + length: number; + gameName: string; + playerNames: string[]; + doNotHide: boolean; +} diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts index 5e8a83153..fd71fece1 100644 --- a/webclient/src/websocket/commands/admin/adjustMod.ts +++ b/webclient/src/websocket/commands/admin/adjustMod.ts @@ -1,26 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { AdminPersistence } from '../../persistence'; export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { - const command = webClient.protobuf.controller.Command_AdjustMod.create({ userName, shouldBeMod, shouldBeJudge }); - const sc = webClient.protobuf.controller.AdminCommand.create({ '.Command_AdjustMod.ext': command }); - - webClient.protobuf.sendAdminCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); - return; - default: - error = 'Failed to reload config.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendAdminCommand('Command_AdjustMod', { userName, shouldBeMod, shouldBeJudge }, { + onSuccess: () => { + AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); + }, }); } diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts index 16d2a574b..979f3ec73 100644 --- a/webclient/src/websocket/commands/admin/reloadConfig.ts +++ b/webclient/src/websocket/commands/admin/reloadConfig.ts @@ -1,26 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { AdminPersistence } from '../../persistence'; export function reloadConfig(): void { - const command = webClient.protobuf.controller.Command_ReloadConfig.create(); - const sc = webClient.protobuf.controller.AdminCommand.create({ '.Command_ReloadConfig.ext': command }); - - webClient.protobuf.sendAdminCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - AdminPersistence.reloadConfig(); - return; - default: - error = 'Failed to reload config.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendAdminCommand('Command_ReloadConfig', {}, { + onSuccess: () => { + AdminPersistence.reloadConfig(); + }, }); } diff --git a/webclient/src/websocket/commands/admin/shutdownServer.ts b/webclient/src/websocket/commands/admin/shutdownServer.ts index 0624e823d..e65c900db 100644 --- a/webclient/src/websocket/commands/admin/shutdownServer.ts +++ b/webclient/src/websocket/commands/admin/shutdownServer.ts @@ -1,26 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { AdminPersistence } from '../../persistence'; export function shutdownServer(reason: string, minutes: number): void { - const command = webClient.protobuf.controller.Command_ShutdownServer.create({ reason, minutes }); - const sc = webClient.protobuf.controller.AdminCommand.create({ '.Command_ShutdownServer.ext': command }); - - webClient.protobuf.sendAdminCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - AdminPersistence.shutdownServer(); - return; - default: - error = 'Failed to update server message.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendAdminCommand('Command_ShutdownServer', { reason, minutes }, { + onSuccess: () => { + AdminPersistence.shutdownServer(); + }, }); } diff --git a/webclient/src/websocket/commands/admin/updateServerMessage.ts b/webclient/src/websocket/commands/admin/updateServerMessage.ts index cb025f5a9..e2b194514 100644 --- a/webclient/src/websocket/commands/admin/updateServerMessage.ts +++ b/webclient/src/websocket/commands/admin/updateServerMessage.ts @@ -1,26 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { AdminPersistence } from '../../persistence'; export function updateServerMessage(): void { - const command = webClient.protobuf.controller.Command_UpdateServerMessage.create(); - const sc = webClient.protobuf.controller.AdminCommand.create({ '.Command_UpdateServerMessage.ext': command }); - - webClient.protobuf.sendAdminCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - AdminPersistence.updateServerMessage(); - return; - default: - error = 'Failed to update server message.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendAdminCommand('Command_UpdateServerMessage', {}, { + onSuccess: () => { + AdminPersistence.updateServerMessage(); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts index 028eb4606..e45e34504 100644 --- a/webclient/src/websocket/commands/moderator/banFromServer.ts +++ b/webclient/src/websocket/commands/moderator/banFromServer.ts @@ -1,29 +1,13 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, visibleReason?: string, clientid?: string, removeMessages?: number): void { - const command = webClient.protobuf.controller.Command_BanFromServer.create({ + BackendService.sendModeratorCommand('Command_BanFromServer', { minutes, userName, address, reason, visibleReason, clientid, removeMessages - }); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_BanFromServer.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - ModeratorPersistence.banFromServer(userName); - return; - default: - error = 'Failed to ban user.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + }, { + onSuccess: () => { + ModeratorPersistence.banFromServer(userName); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts new file mode 100644 index 000000000..d4138a015 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/forceActivateUser.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { + BackendService.sendModeratorCommand('Command_ForceActivateUser', { usernameToActivate, moderatorName }, { + onSuccess: () => { + ModeratorPersistence.forceActivateUser(usernameToActivate, moderatorName); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getAdminNotes.ts b/webclient/src/websocket/commands/moderator/getAdminNotes.ts new file mode 100644 index 000000000..d4f626aa2 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/getAdminNotes.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function getAdminNotes(userName: string): void { + BackendService.sendModeratorCommand('Command_GetAdminNotes', { userName }, { + responseName: 'Response_GetAdminNotes', + onSuccess: (response) => { + ModeratorPersistence.getAdminNotes(userName, response.notes); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/getBanHistory.ts b/webclient/src/websocket/commands/moderator/getBanHistory.ts index 3378f5842..dd4e90eda 100644 --- a/webclient/src/websocket/commands/moderator/getBanHistory.ts +++ b/webclient/src/websocket/commands/moderator/getBanHistory.ts @@ -1,27 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; export function getBanHistory(userName: string): void { - const command = webClient.protobuf.controller.Command_GetBanHistory.create({ userName }); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_GetBanHistory.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { banList } = raw['.Response_BanHistory.ext']; - ModeratorPersistence.banHistory(userName, banList); - return; - default: - error = 'Failed to get ban history.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendModeratorCommand('Command_GetBanHistory', { userName }, { + responseName: 'Response_BanHistory', + onSuccess: (response) => { + ModeratorPersistence.banHistory(userName, response.banList); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/getWarnHistory.ts b/webclient/src/websocket/commands/moderator/getWarnHistory.ts index fce008eec..c47e2c6e4 100644 --- a/webclient/src/websocket/commands/moderator/getWarnHistory.ts +++ b/webclient/src/websocket/commands/moderator/getWarnHistory.ts @@ -1,27 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; export function getWarnHistory(userName: string): void { - const command = webClient.protobuf.controller.Command_GetWarnHistory.create({ userName }); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_GetWarnHistory.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { warnList } = raw['.Response_WarnHistory.ext']; - ModeratorPersistence.warnHistory(userName, warnList); - return; - default: - error = 'Failed to get warn history.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendModeratorCommand('Command_GetWarnHistory', { userName }, { + responseName: 'Response_WarnHistory', + onSuccess: (response) => { + ModeratorPersistence.warnHistory(userName, response.warnList); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/getWarnList.ts b/webclient/src/websocket/commands/moderator/getWarnList.ts index f83e5eb1b..412aee09e 100644 --- a/webclient/src/websocket/commands/moderator/getWarnList.ts +++ b/webclient/src/websocket/commands/moderator/getWarnList.ts @@ -1,27 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; export function getWarnList(modName: string, userName: string, userClientid: string): void { - const command = webClient.protobuf.controller.Command_GetWarnList.create({ modName, userName, userClientid }); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_GetWarnList.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { warning } = raw['.Response_WarnList.ext']; - ModeratorPersistence.warnListOptions(warning); - return; - default: - error = 'Failed to get warn list.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendModeratorCommand('Command_GetWarnList', { modName, userName, userClientid }, { + responseName: 'Response_WarnList', + onSuccess: (response) => { + ModeratorPersistence.warnListOptions(response.warning); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts new file mode 100644 index 000000000..74d64d17a --- /dev/null +++ b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function grantReplayAccess(replayId: number, moderatorName: string): void { + BackendService.sendModeratorCommand('Command_GrantReplayAccess', { replayId, moderatorName }, { + onSuccess: () => { + ModeratorPersistence.grantReplayAccess(replayId, moderatorName); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/index.ts b/webclient/src/websocket/commands/moderator/index.ts index cb4f2455c..10bb0e1c6 100644 --- a/webclient/src/websocket/commands/moderator/index.ts +++ b/webclient/src/websocket/commands/moderator/index.ts @@ -1,6 +1,10 @@ export * from './banFromServer'; +export * from './forceActivateUser'; +export * from './getAdminNotes'; export * from './getBanHistory'; export * from './getWarnHistory'; export * from './getWarnList'; +export * from './grantReplayAccess'; +export * from './updateAdminNotes'; export * from './viewLogHistory'; export * from './warnUser'; diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts new file mode 100644 index 000000000..c7ac315c5 --- /dev/null +++ b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { ModeratorPersistence } from '../../persistence'; + +export function updateAdminNotes(userName: string, notes: string): void { + BackendService.sendModeratorCommand('Command_UpdateAdminNotes', { userName, notes }, { + onSuccess: () => { + ModeratorPersistence.updateAdminNotes(userName, notes); + }, + }); +} diff --git a/webclient/src/websocket/commands/moderator/viewLogHistory.ts b/webclient/src/websocket/commands/moderator/viewLogHistory.ts index 29e3c4bd6..19a930608 100644 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -1,28 +1,12 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; import { LogFilters } from 'types'; export function viewLogHistory(filters: LogFilters): void { - const command = webClient.protobuf.controller.Command_ViewLogHistory.create(filters); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_ViewLogHistory.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { logMessage } = raw['.Response_ViewLogHistory.ext']; - ModeratorPersistence.viewLogs(logMessage) - return; - default: - error = 'Failed to retrieve log history.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendModeratorCommand('Command_ViewLogHistory', filters, { + responseName: 'Response_ViewLogHistory', + onSuccess: (response) => { + ModeratorPersistence.viewLogs(response.logMessage); + }, }); } diff --git a/webclient/src/websocket/commands/moderator/warnUser.ts b/webclient/src/websocket/commands/moderator/warnUser.ts index bef9ee003..0e0271d4b 100644 --- a/webclient/src/websocket/commands/moderator/warnUser.ts +++ b/webclient/src/websocket/commands/moderator/warnUser.ts @@ -1,26 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; -export function warnUser(userName: string, reason: string, clientid?: string, removeMessage?: boolean): void { - const command = webClient.protobuf.controller.Command_WarnUser.create({ userName, reason, clientid, removeMessage }); - const sc = webClient.protobuf.controller.ModeratorCommand.create({ '.Command_WarnUser.ext': command }); - - webClient.protobuf.sendModeratorCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - ModeratorPersistence.warnUser(userName); - return; - default: - error = 'Failed to warn user.'; - break; - } - - if (error) { - console.error(responseCode, error); - } +export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { + BackendService.sendModeratorCommand('Command_WarnUser', { userName, reason, clientid, removeMessages }, { + onSuccess: () => { + ModeratorPersistence.warnUser(userName); + }, }); } diff --git a/webclient/src/websocket/commands/room/createGame.ts b/webclient/src/websocket/commands/room/createGame.ts index e5fd66f80..62565e0e6 100644 --- a/webclient/src/websocket/commands/room/createGame.ts +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -1,21 +1,11 @@ +import { BackendService } from '../../services/BackendService'; import { RoomPersistence } from '../../persistence'; -import webClient from '../../WebClient'; import { GameConfig } from 'types'; export function createGame(roomId: number, gameConfig: GameConfig): void { - const command = webClient.protobuf.controller.Command_CreateGame.create(gameConfig); - const rc = webClient.protobuf.controller.RoomCommand.create({ '.Command_CreateGame.ext': command }); - - webClient.protobuf.sendRoomCommand(roomId, rc, (raw) => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - RoomPersistence.gameCreated(roomId); - break; - default: - console.log('Failed to do the thing'); - } + BackendService.sendRoomCommand(roomId, 'Command_CreateGame', gameConfig, { + onSuccess: () => { + RoomPersistence.gameCreated(roomId); + }, }); } - diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts index 19a672924..ef4b1fff2 100644 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -1,21 +1,11 @@ +import { BackendService } from '../../services/BackendService'; import { RoomPersistence } from '../../persistence'; -import webClient from '../../WebClient'; -import { GameConfig, JoinGameParams } from 'types'; +import { JoinGameParams } from 'types'; export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { - const command = webClient.protobuf.controller.Command_JoinGame.create(joinGameParams); - const rc = webClient.protobuf.controller.RoomCommand.create({ '.Command_JoinGame.ext': command }); - - webClient.protobuf.sendRoomCommand(roomId, rc, (raw) => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - RoomPersistence.joinedGame(roomId, joinGameParams.gameId); - break; - default: - console.log('Failed to do the thing'); - } + BackendService.sendRoomCommand(roomId, 'Command_JoinGame', joinGameParams, { + onSuccess: () => { + RoomPersistence.joinedGame(roomId, joinGameParams.gameId); + }, }); } - diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts index 477ee39a0..7cd64a0e2 100644 --- a/webclient/src/websocket/commands/room/leaveRoom.ts +++ b/webclient/src/websocket/commands/room/leaveRoom.ts @@ -1,19 +1,10 @@ +import { BackendService } from '../../services/BackendService'; import { RoomPersistence } from '../../persistence'; -import webClient from '../../WebClient'; export function leaveRoom(roomId: number): void { - const command = webClient.protobuf.controller.Command_LeaveRoom.create(); - const rc = webClient.protobuf.controller.RoomCommand.create({ '.Command_LeaveRoom.ext': command }); - - webClient.protobuf.sendRoomCommand(roomId, rc, (raw) => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - RoomPersistence.leaveRoom(roomId); - break; - default: - console.log(`Failed to leave Room ${roomId} [${responseCode}] : `, raw); - } + BackendService.sendRoomCommand(roomId, 'Command_LeaveRoom', {}, { + onSuccess: () => { + RoomPersistence.leaveRoom(roomId); + }, }); } diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts index c06abd3ab..a429845be 100644 --- a/webclient/src/websocket/commands/room/roomSay.ts +++ b/webclient/src/websocket/commands/room/roomSay.ts @@ -1,4 +1,4 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; export function roomSay(roomId: number, message: string): void { const trimmed = message.trim(); @@ -7,8 +7,5 @@ export function roomSay(roomId: number, message: string): void { return; } - const command = webClient.protobuf.controller.Command_RoomSay.create({ 'message': trimmed }); - const rc = webClient.protobuf.controller.RoomCommand.create({ '.Command_RoomSay.ext': command }); - - webClient.protobuf.sendRoomCommand(roomId, rc); + BackendService.sendRoomCommand(roomId, 'Command_RoomSay', { message: trimmed }, {}); } diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts index f9210cb9c..31bf2d3f6 100644 --- a/webclient/src/websocket/commands/session/accountEdit.ts +++ b/webclient/src/websocket/commands/session/accountEdit.ts @@ -1,25 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - const command = webClient.protobuf.controller.Command_AccountEdit.create({ passwordCheck, realName, email, country }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_AccountEdit.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.accountEditChanged(realName, email, country); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespFunctionNotAllowed: - console.log('Not allowed'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - console.log('Wrong password'); - break; - default: - console.log('Failed to update information'); - } + BackendService.sendSessionCommand('Command_AccountEdit', { passwordCheck, realName, email, country }, { + onSuccess: () => { + SessionPersistence.accountEditChanged(realName, email, country); + }, }); } diff --git a/webclient/src/websocket/commands/session/accountImage.ts b/webclient/src/websocket/commands/session/accountImage.ts index b735696a9..cd0e24403 100644 --- a/webclient/src/websocket/commands/session/accountImage.ts +++ b/webclient/src/websocket/commands/session/accountImage.ts @@ -1,27 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; -import { common } from 'protobufjs'; -import IBytesValue = common.IBytesValue; -export function accountImage(image: IBytesValue): void { - const command = webClient.protobuf.controller.Command_AccountImage.create({ image }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_AccountImage.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.accountImageChanged(image); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespFunctionNotAllowed: - console.log('Not allowed'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - console.log('Wrong password'); - break; - default: - console.log('Failed to update information'); - } +export function accountImage(image: Uint8Array): void { + BackendService.sendSessionCommand('Command_AccountImage', { image }, { + onSuccess: () => { + SessionPersistence.accountImageChanged(image); + }, }); } diff --git a/webclient/src/websocket/commands/session/accountPassword.ts b/webclient/src/websocket/commands/session/accountPassword.ts index 0b5ef0111..81c7a993b 100644 --- a/webclient/src/websocket/commands/session/accountPassword.ts +++ b/webclient/src/websocket/commands/session/accountPassword.ts @@ -1,19 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - const command = webClient.protobuf.controller.Command_AccountPassword.create({ oldPassword, newPassword, hashedNewPassword }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_AccountPassword.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.accountPasswordChange(); - break; - default: - console.log('Failed to change password'); - } + BackendService.sendSessionCommand('Command_AccountPassword', { oldPassword, newPassword, hashedNewPassword }, { + onSuccess: () => { + SessionPersistence.accountPasswordChange(); + }, }); } diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts index 74e41afeb..4cd0e8c4e 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -2,6 +2,8 @@ import { AccountActivationParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; import { SessionPersistence } from '../../persistence'; import { disconnect, login, updateStatus } from './'; @@ -9,23 +11,21 @@ import { disconnect, login, updateStatus } from './'; export function activate(options: WebSocketConnectOptions, passwordSalt?: string): void { const { userName, token } = options as unknown as AccountActivationParams; - const accountActivationConfig = { + BackendService.sendSessionCommand('Command_Activate', { ...webClient.clientConfig, userName, token, - }; - - const command = webClient.protobuf.controller.Command_Activate.create(accountActivationConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_Activate.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) { - SessionPersistence.accountActivationSuccess(); - login(options, passwordSalt); - } else { + }, { + onResponseCode: { + [ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => { + SessionPersistence.accountActivationSuccess(); + login(options, passwordSalt); + }, + }, + onError: () => { updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); disconnect(); SessionPersistence.accountActivationFailed(); - } + }, }); } diff --git a/webclient/src/websocket/commands/session/addToList.ts b/webclient/src/websocket/commands/session/addToList.ts index 6696d39d4..c5bc3c5f0 100644 --- a/webclient/src/websocket/commands/session/addToList.ts +++ b/webclient/src/websocket/commands/session/addToList.ts @@ -1,4 +1,4 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function addToBuddyList(userName: string): void { @@ -10,16 +10,9 @@ export function addToIgnoreList(userName: string): void { } export function addToList(list: string, userName: string): void { - const command = webClient.protobuf.controller.Command_AddToList.create({ list, userName }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_AddToList.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, ({ responseCode }) => { - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.addToList(list, userName); - break; - default: - console.error('Failed to add to list', responseCode); - } + BackendService.sendSessionCommand('Command_AddToList', { list, userName }, { + onSuccess: () => { + SessionPersistence.addToList(list, userName); + }, }); } diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts index c77d89aa3..752ce78d5 100644 --- a/webclient/src/websocket/commands/session/deckDel.ts +++ b/webclient/src/websocket/commands/session/deckDel.ts @@ -1,19 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function deckDel(deckId: number): void { - const command = webClient.protobuf.controller.Command_DeckDel.create({ deckId }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckDel.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckDelete(deckId); - break; - default: - console.log('Failed to do the thing'); - } + BackendService.sendSessionCommand('Command_DeckDel', { deckId }, { + onSuccess: () => { + SessionPersistence.deleteServerDeck(deckId); + }, }); } diff --git a/webclient/src/websocket/commands/session/deckDelDir.ts b/webclient/src/websocket/commands/session/deckDelDir.ts index ff4b25ec3..df5bbc223 100644 --- a/webclient/src/websocket/commands/session/deckDelDir.ts +++ b/webclient/src/websocket/commands/session/deckDelDir.ts @@ -1,19 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function deckDelDir(path: string): void { - const command = webClient.protobuf.controller.Command_DeckDelDir.create({ path }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckDelDir.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckDeleteDir(path); - break; - default: - console.log('Failed to do the thing'); - } + BackendService.sendSessionCommand('Command_DeckDelDir', { path }, { + onSuccess: () => { + SessionPersistence.deleteServerDeckDir(path); + }, }); } diff --git a/webclient/src/websocket/commands/session/deckDownload.ts b/webclient/src/websocket/commands/session/deckDownload.ts deleted file mode 100644 index a0c4bd461..000000000 --- a/webclient/src/websocket/commands/session/deckDownload.ts +++ /dev/null @@ -1,19 +0,0 @@ -import webClient from '../../WebClient'; -import { SessionPersistence } from '../../persistence'; - -export function deckDownload(deckId: number): void { - const command = webClient.protobuf.controller.Command_DeckDownload.create({ deckId }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckDownload.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckDownload(deckId); - break; - default: - console.log('Failed to do the thing'); - } - }); -} diff --git a/webclient/src/websocket/commands/session/deckList.ts b/webclient/src/websocket/commands/session/deckList.ts index e84b8efb3..3d5a3499a 100644 --- a/webclient/src/websocket/commands/session/deckList.ts +++ b/webclient/src/websocket/commands/session/deckList.ts @@ -1,22 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function deckList(): void { - const command = webClient.protobuf.controller.Command_DeckList.create(); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckList.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - const response = raw['.Response_DeckList.ext']; - - if (response) { - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckList(response); - break; - default: - console.log('Failed to do the thing'); - } - } + BackendService.sendSessionCommand('Command_DeckList', {}, { + responseName: 'Response_DeckList', + onSuccess: (response) => { + SessionPersistence.updateServerDecks(response); + }, }); } diff --git a/webclient/src/websocket/commands/session/deckNewDir.ts b/webclient/src/websocket/commands/session/deckNewDir.ts index b9bca6208..85ab16afb 100644 --- a/webclient/src/websocket/commands/session/deckNewDir.ts +++ b/webclient/src/websocket/commands/session/deckNewDir.ts @@ -1,19 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function deckNewDir(path: string, dirName: string): void { - const command = webClient.protobuf.controller.Command_DeckNewDir.create({ path, dirName }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckNewDir.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckNewDir(path, dirName); - break; - default: - console.log('Failed to do the thing'); - } + BackendService.sendSessionCommand('Command_DeckNewDir', { path, dirName }, { + onSuccess: () => { + SessionPersistence.createServerDeckDir(path, dirName); + }, }); } diff --git a/webclient/src/websocket/commands/session/deckUpload.ts b/webclient/src/websocket/commands/session/deckUpload.ts index af526c8fc..2679c4e8e 100644 --- a/webclient/src/websocket/commands/session/deckUpload.ts +++ b/webclient/src/websocket/commands/session/deckUpload.ts @@ -1,23 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function deckUpload(path: string, deckId: number, deckList: string): void { - const command = webClient.protobuf.controller.Command_DeckUpload.create({ path, deckId, deckList }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_DeckUpload.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - const response = raw['.Response_DeckUpload.ext']; - - if (response) { - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.deckUpload(response); - break; - default: - console.log('Failed to do the thing'); - } - } - + BackendService.sendSessionCommand('Command_DeckUpload', { path, deckId, deckList }, { + responseName: 'Response_DeckUpload', + onSuccess: (response) => { + SessionPersistence.uploadServerDeck(path, response.newFile); + }, }); } diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts index f6399e6a7..05af1ccf9 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -2,30 +2,27 @@ import { ForgotPasswordChallengeParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; import { disconnect, updateStatus } from './'; export function forgotPasswordChallenge(options: WebSocketConnectOptions): void { const { userName, email } = options as unknown as ForgotPasswordChallengeParams; - const forgotPasswordChallengeConfig = { + BackendService.sendSessionCommand('Command_ForgotPasswordChallenge', { ...webClient.clientConfig, userName, email, - }; - - const command = webClient.protobuf.controller.Command_ForgotPasswordChallenge.create(forgotPasswordChallengeConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_ForgotPasswordChallenge.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + }, { + onSuccess: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); - } else { + disconnect(); + }, + onError: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); - } - - disconnect(); + disconnect(); + }, }); } diff --git a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts index 5e5fe3bbd..23d301450 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -2,6 +2,7 @@ import { ForgotPasswordParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; import { disconnect, updateStatus } from './'; @@ -9,30 +10,25 @@ import { disconnect, updateStatus } from './'; export function forgotPasswordRequest(options: WebSocketConnectOptions): void { const { userName } = options as unknown as ForgotPasswordParams; - const forgotPasswordConfig = { + BackendService.sendSessionCommand('Command_ForgotPasswordRequest', { ...webClient.clientConfig, userName, - }; - - const command = webClient.protobuf.controller.Command_ForgotPasswordRequest.create(forgotPasswordConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_ForgotPasswordRequest.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { - const resp = raw['.Response_ForgotPasswordRequest.ext']; - - if (resp.challengeEmail) { + }, { + responseName: 'Response_ForgotPasswordRequest', + onSuccess: (resp) => { + if (resp?.challengeEmail) { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordChallenge(); } else { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); } - } else { + disconnect(); + }, + onError: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); - } - - disconnect(); + disconnect(); + }, }); } diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts index 9312b71af..d9a775816 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -2,6 +2,7 @@ import { ForgotPasswordResetParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; @@ -10,30 +11,28 @@ import { disconnect, updateStatus } from '.'; export function forgotPasswordReset(options: WebSocketConnectOptions, passwordSalt?: string): void { const { userName, token, newPassword } = options as unknown as ForgotPasswordResetParams; - const forgotPasswordResetConfig: any = { + const params: any = { ...webClient.clientConfig, userName, token, }; if (passwordSalt) { - forgotPasswordResetConfig.hashedNewPassword = hashPassword(passwordSalt, newPassword); + params.hashedNewPassword = hashPassword(passwordSalt, newPassword); } else { - forgotPasswordResetConfig.newPassword = newPassword; + params.newPassword = newPassword; } - const command = webClient.protobuf.controller.Command_ForgotPasswordReset.create(forgotPasswordResetConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_ForgotPasswordReset.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + BackendService.sendSessionCommand('Command_ForgotPasswordReset', params, { + onSuccess: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordSuccess(); - } else { + disconnect(); + }, + onError: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); - } - - disconnect(); + disconnect(); + }, }); } diff --git a/webclient/src/websocket/commands/session/getGamesOfUser.ts b/webclient/src/websocket/commands/session/getGamesOfUser.ts index 507ba3e20..8fb8aeb5b 100644 --- a/webclient/src/websocket/commands/session/getGamesOfUser.ts +++ b/webclient/src/websocket/commands/session/getGamesOfUser.ts @@ -1,26 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function getGamesOfUser(userName: string): void { - const command = webClient.protobuf.controller.Command_GetGamesOfUser.create({ userName }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_GetGamesOfUser.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - const response = raw['.Response_GetGamesOfUser.ext']; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.getGamesOfUser(userName, response); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespFunctionNotAllowed: - console.log('Not allowed'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - console.log('Wrong password'); - break; - default: - console.log('Failed to update information'); - } + BackendService.sendSessionCommand('Command_GetGamesOfUser', { userName }, { + responseName: 'Response_GetGamesOfUser', + onSuccess: (response) => { + SessionPersistence.getGamesOfUser(userName, response); + }, }); } diff --git a/webclient/src/websocket/commands/session/getUserInfo.ts b/webclient/src/websocket/commands/session/getUserInfo.ts index 709cc0baa..5b0f178ae 100644 --- a/webclient/src/websocket/commands/session/getUserInfo.ts +++ b/webclient/src/websocket/commands/session/getUserInfo.ts @@ -1,26 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function getUserInfo(userName: string): void { - const command = webClient.protobuf.controller.Command_GetUserInfo.create({ userName }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_GetUserInfo.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { userInfo } = raw['.Response_GetUserInfo.ext']; - SessionPersistence.getUserInfo(userInfo); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespFunctionNotAllowed: - console.log('Not allowed'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - console.log('Wrong password'); - break; - default: - console.log('Failed to update information'); - } + BackendService.sendSessionCommand('Command_GetUserInfo', { userName }, { + responseName: 'Response_GetUserInfo', + onSuccess: (response) => { + SessionPersistence.getUserInfo(response.userInfo); + }, }); } diff --git a/webclient/src/websocket/commands/session/index.ts b/webclient/src/websocket/commands/session/index.ts index 829a74b71..74d0d062c 100644 --- a/webclient/src/websocket/commands/session/index.ts +++ b/webclient/src/websocket/commands/session/index.ts @@ -6,12 +6,11 @@ export * from './addToList'; export * from './connect'; export * from './deckDel'; export * from './deckDelDir'; -export * from './deckDownload'; export * from './deckList'; export * from './deckNewDir'; export * from './deckUpload'; export * from './disconnect'; -export * from './forgotPasswordChallenge' +export * from './forgotPasswordChallenge'; export * from './forgotPasswordRequest'; export * from './forgotPasswordReset'; export * from './getGamesOfUser'; @@ -24,12 +23,8 @@ export * from './message'; export * from './ping'; export * from './register'; export * from './removeFromList'; +export * from './replayDeleteMatch'; +export * from './replayList'; +export * from './replayModifyMatch'; export * from './requestPasswordSalt'; export * from './updateStatus'; - -/** TODO - * REPLAY_DELETE_MATCH - * REPLAY_DOWNLOAD - * REPLAY_LIST - * REPLAY_MODIFY_MATCH - */ diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts index 9f4699708..be79976a0 100644 --- a/webclient/src/websocket/commands/session/joinRoom.ts +++ b/webclient/src/websocket/commands/session/joinRoom.ts @@ -1,37 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { RoomPersistence } from '../../persistence'; export function joinRoom(roomId: number): void { - const command = webClient.protobuf.controller.Command_JoinRoom.create({ roomId }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_JoinRoom.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, (raw) => { - const { responseCode } = raw; - - let error: string; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - const { roomInfo } = raw['.Response_JoinRoom.ext']; - - RoomPersistence.joinRoom(roomInfo); - return; - case webClient.protobuf.controller.Response.ResponseCode.RespNameNotFound: - error = 'Failed to join the room: it doesn\'t exist on the server.'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespContextError: - error = 'The server thinks you are in the room but Cockatrice is unable to display it. Try restarting Cockatrice.'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespUserLevelTooLow: - error = 'You do not have the required permission to join this room.'; - break; - default: - error = 'Failed to join the room due to an unknown error.'; - break; - } - - if (error) { - console.error(responseCode, error); - } + BackendService.sendSessionCommand('Command_JoinRoom', { roomId }, { + responseName: 'Response_JoinRoom', + onSuccess: (response) => { + RoomPersistence.joinRoom(response.roomInfo); + }, }); } diff --git a/webclient/src/websocket/commands/session/listRooms.ts b/webclient/src/websocket/commands/session/listRooms.ts index b1e31621f..367dada9b 100644 --- a/webclient/src/websocket/commands/session/listRooms.ts +++ b/webclient/src/websocket/commands/session/listRooms.ts @@ -1,8 +1,5 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; export function listRooms(): void { - const command = webClient.protobuf.controller.Command_ListRooms.create(); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_ListRooms.ext': command }); - - webClient.protobuf.sendSessionCommand(sc); + BackendService.sendSessionCommand('Command_ListRooms', {}, {}); } diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts index 027a251d4..9b95c1344 100644 --- a/webclient/src/websocket/commands/session/listUsers.ts +++ b/webclient/src/websocket/commands/session/listUsers.ts @@ -1,23 +1,11 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function listUsers(): void { - const command = webClient.protobuf.controller.Command_ListUsers.create(); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_ListUsers.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - const response = raw['.Response_ListUsers.ext']; - - if (response) { - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.updateUsers(response.userList); - break; - default: - console.log(`Failed to fetch Server Rooms [${responseCode}] : `, raw); - } - } - + BackendService.sendSessionCommand('Command_ListUsers', {}, { + responseName: 'Response_ListUsers', + onSuccess: (response) => { + SessionPersistence.updateUsers(response.userList); + }, }); } diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts index db24b4f46..6f3ec5ef5 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -1,5 +1,7 @@ import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; import { hashPassword } from '../../utils'; import { SessionPersistence } from '../../persistence'; @@ -25,13 +27,18 @@ export function login(options: WebSocketConnectOptions, passwordSalt?: string): loginConfig.password = password; } - const command = webClient.protobuf.controller.Command_Login.create(loginConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_Login.ext': command }); + const { ResponseCode } = ProtoController.root.Response; - webClient.protobuf.sendSessionCommand(sc, raw => { - const resp = raw['.Response_Login.ext']; + const onLoginError = (message: string, extra?: () => void) => { + updateStatus(StatusEnum.DISCONNECTED, message); + extra?.(); + SessionPersistence.loginFailed(); + disconnect(); + }; - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { + BackendService.sendSessionCommand('Command_Login', loginConfig, { + responseName: 'Response_Login', + onSuccess: (resp) => { const { buddyList, ignoreList, userInfo } = resp; SessionPersistence.updateBuddyList(buddyList); @@ -43,50 +50,30 @@ export function login(options: WebSocketConnectOptions, passwordSalt?: string): listRooms(); updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); - - return; - } - - switch (raw.responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespClientUpdateRequired: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: missing features'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - case webClient.protobuf.controller.Response.ResponseCode.RespUsernameInvalid: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: incorrect username or password'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespWouldOverwriteOldSession: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: duplicated user session'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespUserIsBanned: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: banned user'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationRequired: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespClientIdRequired: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: missing client ID'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespContextError: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: server error'); - break; - - case webClient.protobuf.controller.Response.ResponseCode.RespAccountNotActivated: - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: account not activated'); - SessionPersistence.accountAwaitingActivation(options); - break; - - default: - updateStatus(StatusEnum.DISCONNECTED, `Login failed: unknown error: ${raw.responseCode}`); - } - - SessionPersistence.loginFailed(); - disconnect(); + }, + onResponseCode: { + [ResponseCode.RespClientUpdateRequired]: () => + onLoginError('Login failed: missing features'), + [ResponseCode.RespWrongPassword]: () => + onLoginError('Login failed: incorrect username or password'), + [ResponseCode.RespUsernameInvalid]: () => + onLoginError('Login failed: incorrect username or password'), + [ResponseCode.RespWouldOverwriteOldSession]: () => + onLoginError('Login failed: duplicated user session'), + [ResponseCode.RespUserIsBanned]: () => + onLoginError('Login failed: banned user'), + [ResponseCode.RespRegistrationRequired]: () => + onLoginError('Login failed: registration required'), + [ResponseCode.RespClientIdRequired]: () => + onLoginError('Login failed: missing client ID'), + [ResponseCode.RespContextError]: () => + onLoginError('Login failed: server error'), + [ResponseCode.RespAccountNotActivated]: () => + onLoginError('Login failed: account not activated', + () => SessionPersistence.accountAwaitingActivation(options) + ), + }, + onError: (responseCode) => + onLoginError(`Login failed: unknown error: ${responseCode}`), }); } diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts index 99b615562..075fc3c4b 100644 --- a/webclient/src/websocket/commands/session/message.ts +++ b/webclient/src/websocket/commands/session/message.ts @@ -1,31 +1,10 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function message(userName: string, message: string): void { - const command = webClient.protobuf.controller.Command_Message.create({ userName, message }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_Message.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - const { responseCode } = raw; - - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.directMessageSent(userName, message); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespNameNotFound: - console.log('Name not found'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespInIgnoreList: - console.log('On ignore list'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespChatFlood: - console.log('Flooding chat'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespWrongPassword: - console.log('Wrong password'); - break; - default: - console.log('Failed to send direct message'); - } + BackendService.sendSessionCommand('Command_Message', { userName, message }, { + onSuccess: () => { + SessionPersistence.directMessageSent(userName, message); + }, }); } diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts index 795e0f71f..fea2784a2 100644 --- a/webclient/src/websocket/commands/session/ping.ts +++ b/webclient/src/websocket/commands/session/ping.ts @@ -1,8 +1,7 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; export function ping(pingReceived: Function): void { - const command = webClient.protobuf.controller.Command_Ping.create(); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_Ping.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, pingReceived); + BackendService.sendSessionCommand('Command_Ping', {}, { + onResponse: (raw) => pingReceived(raw), + }); } diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index 45b2fa977..a25b85868 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -1,17 +1,18 @@ import { ServerRegisterParams } from 'store'; -import { WebSocketConnectOptions } from 'types'; +import { StatusEnum, WebSocketConnectOptions } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; -import NormalizeService from '../../utils/NormalizeService'; -import { login, disconnect } from './'; +import { login, disconnect, updateStatus } from './'; export function register(options: WebSocketConnectOptions, passwordSalt?: string): void { const { userName, password, email, country, realName } = options as ServerRegisterParams; - const registerConfig: any = { + const params: any = { ...webClient.clientConfig, userName, email, @@ -20,55 +21,57 @@ export function register(options: WebSocketConnectOptions, passwordSalt?: string }; if (passwordSalt) { - registerConfig.hashedPassword = hashPassword(passwordSalt, password); + params.hashedPassword = hashPassword(passwordSalt, password); } else { - registerConfig.password = password; + params.password = password; } - const command = webClient.protobuf.controller.Command_Register.create(registerConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_Register.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAccepted) { - login(options, passwordSalt); - SessionPersistence.registrationSuccess() - return; - } - - switch (raw.responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation: - SessionPersistence.accountAwaitingActivation(options); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespUserAlreadyExists: - SessionPersistence.registrationUserNameError('Username is taken'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespUsernameInvalid: - SessionPersistence.registrationUserNameError('Invalid username'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespPasswordTooShort: - SessionPersistence.registrationPasswordError('Your password was too short'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespEmailRequiredToRegister: - SessionPersistence.registrationRequiresEmail(); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespEmailBlackListed: - SessionPersistence.registrationEmailError('This email provider has been blocked'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespTooManyRequests: - SessionPersistence.registrationEmailError('Max accounts reached for this email'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled: - SessionPersistence.registrationFailed('Registration is currently disabled'); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespUserIsBanned: - SessionPersistence.registrationFailed(raw.reasonStr, raw.endTime); - break; - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationFailed: - default: - SessionPersistence.registrationFailed('Registration failed due to a server issue'); - break; - } + const { ResponseCode } = ProtoController.root.Response; + const onRegistrationError = (action: () => void) => { + action(); + updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); disconnect(); + }; + + BackendService.sendSessionCommand('Command_Register', params, { + onResponseCode: { + [ResponseCode.RespRegistrationAccepted]: () => { + login(options, passwordSalt); + SessionPersistence.registrationSuccess(); + }, + [ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); + SessionPersistence.accountAwaitingActivation(options); + disconnect(); + }, + [ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + () => SessionPersistence.registrationUserNameError('Username is taken') + ), + [ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + () => SessionPersistence.registrationUserNameError('Invalid username') + ), + [ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + () => SessionPersistence.registrationPasswordError('Your password was too short') + ), + [ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + () => SessionPersistence.registrationRequiresEmail() + ), + [ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + () => SessionPersistence.registrationEmailError('This email provider has been blocked') + ), + [ResponseCode.RespTooManyRequests]: () => onRegistrationError( + () => SessionPersistence.registrationEmailError('Max accounts reached for this email') + ), + [ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + () => SessionPersistence.registrationFailed('Registration is currently disabled') + ), + [ResponseCode.RespUserIsBanned]: (raw) => onRegistrationError( + () => SessionPersistence.registrationFailed(raw.reasonStr, raw.endTime) + ), + }, + onError: () => onRegistrationError( + () => SessionPersistence.registrationFailed('Registration failed due to a server issue') + ), }); } diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts index 222d9c57d..aede49c49 100644 --- a/webclient/src/websocket/commands/session/removeFromList.ts +++ b/webclient/src/websocket/commands/session/removeFromList.ts @@ -1,4 +1,4 @@ -import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; import { SessionPersistence } from '../../persistence'; export function removeFromBuddyList(userName: string): void { @@ -10,16 +10,9 @@ export function removeFromIgnoreList(userName: string): void { } export function removeFromList(list: string, userName: string): void { - const command = webClient.protobuf.controller.Command_RemoveFromList.create({ list, userName }); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_RemoveFromList.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, ({ responseCode }) => { - switch (responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: - SessionPersistence.removeFromList(list, userName); - break; - default: - console.error('Failed to remove from list', responseCode); - } + BackendService.sendSessionCommand('Command_RemoveFromList', { list, userName }, { + onSuccess: () => { + SessionPersistence.removeFromList(list, userName); + }, }); } diff --git a/webclient/src/websocket/commands/session/replayDeleteMatch.ts b/webclient/src/websocket/commands/session/replayDeleteMatch.ts new file mode 100644 index 000000000..24ac48f1c --- /dev/null +++ b/webclient/src/websocket/commands/session/replayDeleteMatch.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayDeleteMatch(gameId: number): void { + BackendService.sendSessionCommand('Command_ReplayDeleteMatch', { gameId }, { + onSuccess: () => { + SessionPersistence.replayDeleteMatch(gameId); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/replayList.ts b/webclient/src/websocket/commands/session/replayList.ts new file mode 100644 index 000000000..f39eb279f --- /dev/null +++ b/webclient/src/websocket/commands/session/replayList.ts @@ -0,0 +1,11 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayList(): void { + BackendService.sendSessionCommand('Command_ReplayList', {}, { + responseName: 'Response_ReplayList', + onSuccess: (response) => { + SessionPersistence.replayList(response.matchList); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/replayModifyMatch.ts b/webclient/src/websocket/commands/session/replayModifyMatch.ts new file mode 100644 index 000000000..9825047f3 --- /dev/null +++ b/webclient/src/websocket/commands/session/replayModifyMatch.ts @@ -0,0 +1,10 @@ +import { BackendService } from '../../services/BackendService'; +import { SessionPersistence } from '../../persistence'; + +export function replayModifyMatch(gameId: number, doNotHide: boolean): void { + BackendService.sendSessionCommand('Command_ReplayModifyMatch', { gameId, doNotHide }, { + onSuccess: () => { + SessionPersistence.replayModifyMatch(gameId, doNotHide); + }, + }); +} diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index b7ab5ef92..a3d1fc05c 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -2,6 +2,8 @@ import { RequestPasswordSaltParams } from 'store'; import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; import webClient from '../../WebClient'; +import { BackendService } from '../../services/BackendService'; +import { ProtoController } from '../../services/ProtoController'; import { SessionPersistence } from '../../persistence'; import { @@ -15,64 +17,48 @@ import { export function requestPasswordSalt(options: WebSocketConnectOptions): void { const { userName } = options as RequestPasswordSaltParams; - const registerConfig = { - ...webClient.clientConfig, - userName, - }; - - const command = webClient.protobuf.controller.Command_RequestPasswordSalt.create(registerConfig); - const sc = webClient.protobuf.controller.SessionCommand.create({ '.Command_RequestPasswordSalt.ext': command }); - - webClient.protobuf.sendSessionCommand(sc, raw => { - switch (raw.responseCode) { - case webClient.protobuf.controller.Response.ResponseCode.RespOk: { - const passwordSalt = raw['.Response_PasswordSalt.ext']?.passwordSalt; - - switch (options.reason) { - case WebSocketConnectReason.ACTIVATE_ACCOUNT: { - activate(options, passwordSalt); - break; - } - - case WebSocketConnectReason.PASSWORD_RESET: { - forgotPasswordReset(options, passwordSalt); - break; - } - - case WebSocketConnectReason.LOGIN: - default: { - login(options, passwordSalt); - } - } - - return; - } - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationRequired: { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); - break; - } - default: { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); - } - } - + const onFailure = () => { switch (options.reason) { - case WebSocketConnectReason.ACTIVATE_ACCOUNT: { + case WebSocketConnectReason.ACTIVATE_ACCOUNT: SessionPersistence.accountActivationFailed(); break; - } - - case WebSocketConnectReason.PASSWORD_RESET: { + case WebSocketConnectReason.PASSWORD_RESET: SessionPersistence.resetPasswordFailed(); break; - } - - case WebSocketConnectReason.LOGIN: - default: { + default: SessionPersistence.loginFailed(); - } } - disconnect(); + }; + + BackendService.sendSessionCommand('Command_RequestPasswordSalt', { + ...webClient.clientConfig, + userName, + }, { + responseName: 'Response_PasswordSalt', + onSuccess: (resp) => { + const passwordSalt = resp?.passwordSalt; + + switch (options.reason) { + case WebSocketConnectReason.ACTIVATE_ACCOUNT: + activate(options, passwordSalt); + break; + case WebSocketConnectReason.PASSWORD_RESET: + forgotPasswordReset(options, passwordSalt); + break; + default: + login(options, passwordSalt); + } + }, + onResponseCode: { + [ProtoController.root.Response.ResponseCode.RespRegistrationRequired]: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); + onFailure(); + }, + }, + onError: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); + onFailure(); + }, }); } diff --git a/webclient/src/websocket/events/game/index.ts b/webclient/src/websocket/events/game/index.ts index 895eadfef..a7b3277a5 100644 --- a/webclient/src/websocket/events/game/index.ts +++ b/webclient/src/websocket/events/game/index.ts @@ -4,8 +4,8 @@ import { leaveGame } from './leaveGame'; export const GameEvents: ProtobufEvents = { - '.Event_Join.ext': () => joinGame, - '.Event_Leave.ext': () => leaveGame, + '.Event_Join.ext': joinGame, + '.Event_Leave.ext': leaveGame, '.Event_GameClosed.ext': () => console.log('Event_GameClosed.ext'), '.Event_GameHostChanged.ext': () => console.log('Event_GameHostChanged.ext'), '.Event_Kicked.ext': () => console.log('Event_Kicked.ext'), diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts index 9c39db940..227113059 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -1,5 +1,5 @@ import { StatusEnum } from 'types'; -import webClient from '../../WebClient'; +import { ProtoController } from '../../services/ProtoController'; import { updateStatus } from '../../commands/session'; import { ConnectionClosedData } from './interfaces'; @@ -10,29 +10,30 @@ export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): v if (reasonStr) { message = reasonStr; } else { + const { CloseReason } = ProtoController.root.Event_ConnectionClosed; switch (reason) { - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USER_LIMIT_REACHED: + case CloseReason.USER_LIMIT_REACHED: message = 'The server has reached its maximum user capacity'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.TOO_MANY_CONNECTIONS: + case CloseReason.TOO_MANY_CONNECTIONS: message = 'There are too many concurrent connections from your address'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.BANNED: + case CloseReason.BANNED: message = 'You are banned'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.DEMOTED: + case CloseReason.DEMOTED: message = 'You were demoted'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.SERVER_SHUTDOWN: + case CloseReason.SERVER_SHUTDOWN: message = 'Scheduled server shutdown'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.USERNAMEINVALID: + case CloseReason.USERNAMEINVALID: message = 'Invalid username'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.LOGGEDINELSEWERE: + case CloseReason.LOGGEDINELSEWERE: message = 'You have been logged out due to logging in at another location'; break; - case webClient.protobuf.controller.Event_ConnectionClosed.CloseReason.OTHER: + case CloseReason.OTHER: default: message = 'Unknown reason'; break; diff --git a/webclient/src/websocket/events/session/index.ts b/webclient/src/websocket/events/session/index.ts index 9fe7bb1da..5b3ab198e 100644 --- a/webclient/src/websocket/events/session/index.ts +++ b/webclient/src/websocket/events/session/index.ts @@ -3,8 +3,9 @@ import { addToList } from './addToList'; import { connectionClosed } from './connectionClosed'; import { listRooms } from './listRooms'; import { notifyUser } from './notifyUser'; -import { playerPropertiesChanged } from '../common/playerPropertiesChanged'; import { removeFromList } from './removeFromList'; +import { replayAdded } from './replayAdded'; +import { serverCompleteList } from './serverCompleteList'; import { serverIdentification } from './serverIdentification'; import { serverMessage } from './serverMessage'; import { serverShutdown } from './serverShutdown'; @@ -20,8 +21,8 @@ export const SessionEvents: ProtobufEvents = { '.Event_ListRooms.ext': listRooms, '.Event_NotifyUser.ext': notifyUser, '.Event_RemoveFromList.ext': removeFromList, - '.Event_ReplayAdded.ext': () => console.log('Event_ReplayAdded'), - '.Event_ServerCompleteList.ext': () => console.log('Event_ServerCompleteList'), + '.Event_ReplayAdded.ext': replayAdded, + '.Event_ServerCompleteList.ext': serverCompleteList, '.Event_ServerIdentification.ext': serverIdentification, '.Event_ServerMessage.ext': serverMessage, '.Event_ServerShutdown.ext': serverShutdown, diff --git a/webclient/src/websocket/events/session/interfaces.ts b/webclient/src/websocket/events/session/interfaces.ts index 8a1827cbd..ddc10d103 100644 --- a/webclient/src/websocket/events/session/interfaces.ts +++ b/webclient/src/websocket/events/session/interfaces.ts @@ -1,4 +1,4 @@ -import { Game, NotificationType, Room, User } from 'types'; +import { Game, NotificationType, ReplayMatch, Room, User } from 'types'; export interface AddToListData { listName: string; @@ -13,6 +13,8 @@ export interface ConnectionClosedData { export interface GameJoinedData { gameInfo: Game; + gameTypes: any[]; + hostId: number; playerId: number; spectator: boolean; resuming: boolean; @@ -76,3 +78,13 @@ export interface UserMessageData { receiverName: string; message: string; } + +export interface ReplayAddedData { + matchInfo: ReplayMatch; +} + +export interface ServerCompleteListData { + serverId: number; + userList: User[]; + roomList: Room[]; +} diff --git a/webclient/src/websocket/events/session/replayAdded.ts b/webclient/src/websocket/events/session/replayAdded.ts new file mode 100644 index 000000000..18a4ea82d --- /dev/null +++ b/webclient/src/websocket/events/session/replayAdded.ts @@ -0,0 +1,6 @@ +import { SessionPersistence } from '../../persistence'; +import { ReplayAddedData } from './interfaces'; + +export function replayAdded({ matchInfo }: ReplayAddedData): void { + SessionPersistence.replayAdded(matchInfo); +} diff --git a/webclient/src/websocket/events/session/serverCompleteList.ts b/webclient/src/websocket/events/session/serverCompleteList.ts new file mode 100644 index 000000000..77d37a31e --- /dev/null +++ b/webclient/src/websocket/events/session/serverCompleteList.ts @@ -0,0 +1,7 @@ +import { RoomPersistence, SessionPersistence } from '../../persistence'; +import { ServerCompleteListData } from './interfaces'; + +export function serverCompleteList({ userList, roomList }: ServerCompleteListData): void { + SessionPersistence.updateUsers(userList); + RoomPersistence.updateRooms(roomList); +} diff --git a/webclient/src/websocket/events/session/serverIdentification.ts b/webclient/src/websocket/events/session/serverIdentification.ts index ce89e1f3c..87ae79453 100644 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -1,4 +1,4 @@ -import { StatusEnum, WebSocketConnectReason } from 'types'; +import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; import webClient from '../../WebClient'; import { @@ -24,48 +24,48 @@ export function serverIdentification(info: ServerIdentificationData): void { return; } - const getPasswordSalt = passwordSaltSupported(serverOptions, webClient); - const { options } = webClient; + const getPasswordSalt = passwordSaltSupported(serverOptions); + const connectOptions = { ...webClient.options }; - switch (options.reason) { + switch (connectOptions.reason) { case WebSocketConnectReason.LOGIN: updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); if (getPasswordSalt) { - requestPasswordSalt(options); + requestPasswordSalt(connectOptions); } else { - login(options); + login(connectOptions); } break; case WebSocketConnectReason.REGISTER: const passwordSalt = getPasswordSalt ? generateSalt() : null; - register(options, passwordSalt); + register(connectOptions, passwordSalt); break; case WebSocketConnectReason.ACTIVATE_ACCOUNT: if (getPasswordSalt) { - requestPasswordSalt(options); + requestPasswordSalt(connectOptions); } else { - activate(options); + activate(connectOptions); } break; case WebSocketConnectReason.PASSWORD_RESET_REQUEST: - forgotPasswordRequest(options); + forgotPasswordRequest(connectOptions); break; case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - forgotPasswordChallenge(options); + forgotPasswordChallenge(connectOptions); break; case WebSocketConnectReason.PASSWORD_RESET: if (getPasswordSalt) { - requestPasswordSalt(options); + requestPasswordSalt(connectOptions); } else { - forgotPasswordReset(options); + forgotPasswordReset(connectOptions); } break; default: - updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Reason: ' + options.reason); + updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Reason: ' + connectOptions.reason); disconnect(); break; } - webClient.options = {}; + webClient.options = {} as WebSocketConnectOptions; SessionPersistence.updateInfo(serverName, serverVersion); } diff --git a/webclient/src/websocket/persistence/AdminPresistence.ts b/webclient/src/websocket/persistence/AdminPersistence.ts similarity index 100% rename from webclient/src/websocket/persistence/AdminPresistence.ts rename to webclient/src/websocket/persistence/AdminPersistence.ts diff --git a/webclient/src/websocket/persistence/ModeratorPresistence.ts b/webclient/src/websocket/persistence/ModeratorPersistence.ts similarity index 61% rename from webclient/src/websocket/persistence/ModeratorPresistence.ts rename to webclient/src/websocket/persistence/ModeratorPersistence.ts index e6a57f601..d1b991fa0 100644 --- a/webclient/src/websocket/persistence/ModeratorPresistence.ts +++ b/webclient/src/websocket/persistence/ModeratorPersistence.ts @@ -27,4 +27,20 @@ export class ModeratorPersistence { static warnUser(userName: string): void { ServerDispatch.warnUser(userName); } + + static grantReplayAccess(replayId: number, moderatorName: string): void { + ServerDispatch.grantReplayAccess(replayId, moderatorName); + } + + static forceActivateUser(usernameToActivate: string, moderatorName: string): void { + ServerDispatch.forceActivateUser(usernameToActivate, moderatorName); + } + + static getAdminNotes(userName: string, notes: string): void { + ServerDispatch.getAdminNotes(userName, notes); + } + + static updateAdminNotes(userName: string, notes: string): void { + ServerDispatch.updateAdminNotes(userName, notes); + } } diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index c9248745c..9962af968 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -1,5 +1,5 @@ import { ServerDispatch } from 'store'; -import { DeckStorageTreeItem, StatusEnum, User, WebSocketConnectOptions } from 'types'; +import { DeckList, DeckStorageTreeItem, ReplayMatch, StatusEnum, User, WebSocketConnectOptions } from 'types'; import { sanitizeHtml } from 'websocket/utils'; import { @@ -10,9 +10,6 @@ import { UserMessageData } from '../events/session/interfaces'; import NormalizeService from '../utils/NormalizeService'; -import { DeckList } from 'types'; -import { common } from 'protobufjs'; -import IBytesValue = common.IBytesValue; export class SessionPersistence { static initialized() { @@ -165,7 +162,7 @@ export class SessionPersistence { ServerDispatch.accountEditChanged({ realName, email, country }); } - static accountImageChanged(avatarBmp: IBytesValue): void { + static accountImageChanged(avatarBmp: Uint8Array): void { ServerDispatch.accountImageChanged({ avatarBmp }); } @@ -178,7 +175,8 @@ export class SessionPersistence { } static getGamesOfUser(userName: string, response: any): void { - console.log('getGamesOfUser'); + // Response_GetGamesOfUser contains a gameList field — log for now until game layer is complete + console.log('getGamesOfUser', userName, response); } static gameJoined(gameJoinedData: GameJoinedData): void { @@ -209,28 +207,40 @@ export class SessionPersistence { ServerDispatch.removeFromList(list, userName); } - static deckDelete(deckId: number): void { - console.log('deckDelete', deckId); + static deleteServerDeck(deckId: number): void { + ServerDispatch.deckDelete(deckId); } - static deckDeleteDir(path: string): void { - console.log('deckDeleteDir', path); + static updateServerDecks(deckList: DeckList): void { + ServerDispatch.backendDecks(deckList); } - static deckDownload(deckId: number): void { - console.log('deckDownload', deckId); + static uploadServerDeck(path: string, treeItem: DeckStorageTreeItem): void { + ServerDispatch.deckUpload(path, treeItem); } - static deckList(deckList: DeckList): void { - console.log('deckList', deckList); + static createServerDeckDir(path: string, dirName: string): void { + ServerDispatch.deckNewDir(path, dirName); } - static deckNewDir(path: string, dirName: string): void { - console.log('deckNewDir', path, dirName); + static deleteServerDeckDir(path: string): void { + ServerDispatch.deckDelDir(path); } - static deckUpload(treeItem: DeckStorageTreeItem): void { - console.log('deckUpload', treeItem); + static replayList(matchList: ReplayMatch[]): void { + ServerDispatch.replayList(matchList); + } + + static replayAdded(matchInfo: ReplayMatch): void { + ServerDispatch.replayAdded(matchInfo); + } + + static replayModifyMatch(gameId: number, doNotHide: boolean): void { + ServerDispatch.replayModifyMatch(gameId, doNotHide); + } + + static replayDeleteMatch(gameId: number): void { + ServerDispatch.replayDeleteMatch(gameId); } } diff --git a/webclient/src/websocket/persistence/index.ts b/webclient/src/websocket/persistence/index.ts index f4df52645..a1e34fffa 100644 --- a/webclient/src/websocket/persistence/index.ts +++ b/webclient/src/websocket/persistence/index.ts @@ -1,5 +1,5 @@ -export { AdminPersistence } from './AdminPresistence'; +export { AdminPersistence } from './AdminPersistence'; export { RoomPersistence } from './RoomPersistence'; export { SessionPersistence } from './SessionPersistence'; -export { ModeratorPersistence } from './ModeratorPresistence'; +export { ModeratorPersistence } from './ModeratorPersistence'; export { GamePersistence } from './GamePersistence'; diff --git a/webclient/src/websocket/services/BackendService.ts b/webclient/src/websocket/services/BackendService.ts new file mode 100644 index 000000000..fce741d35 --- /dev/null +++ b/webclient/src/websocket/services/BackendService.ts @@ -0,0 +1,82 @@ +import webClient from '../WebClient'; +import { ProtoController } from './ProtoController'; + +export interface CommandOptions { + responseName?: string; + onSuccess?: (response: any, raw: any) => void; + onError?: (responseCode: number, raw: any) => void; + onResponseCode?: { [code: number]: (raw: any) => void }; + onResponse?: (raw: any) => void; +} + +export class BackendService { + static sendSessionCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const sc = ProtoController.root.SessionCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendSessionCommand(sc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendRoomCommand(roomId: number, commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const rc = ProtoController.root.RoomCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendRoomCommand(roomId, rc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendModeratorCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const mc = ProtoController.root.ModeratorCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendModeratorCommand(mc, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + static sendAdminCommand(commandName: string, params: any, options: CommandOptions): void { + const command = ProtoController.root[commandName].create(params || {}); + const ac = ProtoController.root.AdminCommand.create({ + [`.${commandName}.ext`]: command, + }); + webClient.protobuf.sendAdminCommand(ac, raw => { + BackendService.handleResponse(commandName, raw, options); + }); + } + + private static handleResponse(commandName: string, raw: any, options: CommandOptions): void { + if (options.onResponse) { + options.onResponse(raw); + return; + } + + const { responseCode } = raw; + + if (responseCode === ProtoController.root.Response.ResponseCode.RespOk) { + if (options.onSuccess) { + const response = options.responseName + ? raw[`.${options.responseName}.ext`] + : raw; + options.onSuccess(response, raw); + } + return; + } + + if (options.onResponseCode?.[responseCode]) { + options.onResponseCode[responseCode](raw); + return; + } + + if (options.onError) { + options.onError(responseCode, raw); + } else { + console.error(`${commandName} failed with response code: ${responseCode}`); + } + } +} diff --git a/webclient/src/websocket/services/ProtoController.ts b/webclient/src/websocket/services/ProtoController.ts new file mode 100644 index 000000000..130d5e196 --- /dev/null +++ b/webclient/src/websocket/services/ProtoController.ts @@ -0,0 +1,24 @@ +import protobuf from 'protobufjs'; + +import { SessionPersistence } from '../persistence'; +import ProtoFiles from '../../proto-files.json'; + +const PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`; + +// Leaf module — no imports from the websocket layer other than persistence. +// Both BackendService and ProtobufService import this; neither should import +// the other for controller access, avoiding circular dependency cycles. +export const ProtoController = { + root: null as any, + + load(): void { + const files = ProtoFiles.map(file => `${PB_FILE_DIR}/${file}`); + ProtoController.root = new protobuf.Root(); + ProtoController.root.load(files, { keepCase: false }, (err: Error) => { + if (err) { + throw err; + } + SessionPersistence.initialized(); + }); + }, +}; diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index 341bbeae3..20c4e8599 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,19 +1,13 @@ -import protobuf from 'protobufjs'; - import { CommonEvents, GameEvents, RoomEvents, SessionEvents } from '../events'; -import { SessionPersistence } from '../persistence'; import { WebClient } from '../WebClient'; import { SessionCommands } from 'websocket'; -import ProtoFiles from '../../proto-files.json'; +import { ProtoController } from './ProtoController'; export interface ProtobufEvents { [event: string]: Function; } export class ProtobufService { - static PB_FILE_DIR = `${process.env.PUBLIC_URL}/pb`; - - public controller; private cmdId = 0; private pendingCommands: { [cmdId: string]: Function } = {}; @@ -21,8 +15,7 @@ export class ProtobufService { constructor(webClient: WebClient) { this.webClient = webClient; - - this.loadProtobufFiles(); + ProtoController.load(); } public resetCommands() { @@ -30,8 +23,8 @@ export class ProtobufService { this.pendingCommands = {}; } - public sendRoomCommand(roomId: number, roomCmd: number, callback?: Function) { - const cmd = this.controller.CommandContainer.create({ + public sendRoomCommand(roomId: number, roomCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ 'roomId': roomId, 'roomCommand': [roomCmd] }); @@ -39,38 +32,38 @@ export class ProtobufService { this.sendCommand(cmd, raw => callback && callback(raw)); } - public sendSessionCommand(sesCmd: number, callback?: Function) { - const cmd = this.controller.CommandContainer.create({ + public sendSessionCommand(sesCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ 'sessionCommand': [sesCmd] }); this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendModeratorCommand(modCmd: number, callback?: Function) { - const cmd = this.controller.CommandContainer.create({ + public sendModeratorCommand(modCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ 'moderatorCommand': [modCmd] }); this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendAdminCommand(adminCmd: number, callback?: Function) { - const cmd = this.controller.CommandContainer.create({ + public sendAdminCommand(adminCmd: any, callback?: Function) { + const cmd = ProtoController.root.CommandContainer.create({ 'adminCommand': [adminCmd] }); this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendCommand(cmd: number, callback: Function) { + public sendCommand(cmd: any, callback: Function) { this.cmdId++; cmd['cmdId'] = this.cmdId; this.pendingCommands[this.cmdId] = callback; if (this.webClient.socket.checkReadyState(WebSocket.OPEN)) { - this.webClient.socket.send(this.controller.CommandContainer.encode(cmd).finish()); + this.webClient.socket.send(ProtoController.root.CommandContainer.encode(cmd).finish()); } } @@ -81,20 +74,20 @@ export class ProtobufService { public handleMessageEvent({ data }: MessageEvent): void { try { const uint8msg = new Uint8Array(data); - const msg = this.controller.ServerMessage.decode(uint8msg); + const msg = ProtoController.root.ServerMessage.decode(uint8msg); if (msg) { switch (msg.messageType) { - case this.controller.ServerMessage.MessageType.RESPONSE: + case ProtoController.root.ServerMessage.MessageType.RESPONSE: this.processServerResponse(msg.response); break; - case this.controller.ServerMessage.MessageType.ROOM_EVENT: + case ProtoController.root.ServerMessage.MessageType.ROOM_EVENT: this.processRoomEvent(msg.roomEvent, msg); break; - case this.controller.ServerMessage.MessageType.SESSION_EVENT: + case ProtoController.root.ServerMessage.MessageType.SESSION_EVENT: this.processSessionEvent(msg.sessionEvent, msg); break; - case this.controller.ServerMessage.MessageType.GAME_EVENT_CONTAINER: + case ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER: this.processGameEvent(msg.gameEvent, msg); break; default: @@ -142,17 +135,4 @@ export class ProtobufService { } } } - - private loadProtobufFiles() { - const files = ProtoFiles.map(file => `${ProtobufService.PB_FILE_DIR}/${file}`); - - this.controller = new protobuf.Root(); - this.controller.load(files, { keepCase: false }, (err, root) => { - if (err) { - throw err; - } - - SessionPersistence.initialized(); - }); - } } diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts index 77645694f..164a91823 100644 --- a/webclient/src/websocket/utils/passwordHasher.ts +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -1,5 +1,6 @@ import sha512 from 'crypto-js/sha512'; import Base64 from 'crypto-js/enc-base64'; +import { ProtoController } from '../services/ProtoController'; const HASH_ROUNDS = 1_000; const SALT_LENGTH = 16; @@ -25,7 +26,7 @@ export const generateSalt = (): string => { return salt; } -export const passwordSaltSupported = (serverOptions, webClient): number => { +export const passwordSaltSupported = (serverOptions: number): number => { // Intentional use of Bitwise operator b/c of how Servatrice Enums work - return serverOptions & webClient.protobuf.controller.Event_ServerIdentification.ServerOptions.SupportsPasswordHash; + return serverOptions & ProtoController.root.Event_ServerIdentification.ServerOptions.SupportsPasswordHash; }