diff options
45 files changed, 1575 insertions, 39 deletions
diff --git a/.travis/common/travis-ci.env b/.travis/common/travis-ci.env index a333786ce..ec8e2dd63 100644 --- a/.travis/common/travis-ci.env +++ b/.travis/common/travis-ci.env | |||
| @@ -10,3 +10,7 @@ TRAVIS_JOB_ID | |||
| 10 | TRAVIS_JOB_NUMBER | 10 | TRAVIS_JOB_NUMBER |
| 11 | TRAVIS_REPO_SLUG | 11 | TRAVIS_REPO_SLUG |
| 12 | TRAVIS_TAG | 12 | TRAVIS_TAG |
| 13 | |||
| 14 | # yuzu specific flags | ||
| 15 | ENABLE_COMPATIBILITY_REPORTING | ||
| 16 | USE_DISCORD_PRESENCE | ||
diff --git a/.travis/linux/build.sh b/.travis/linux/build.sh index c3a1f93e4..2fced727d 100755 --- a/.travis/linux/build.sh +++ b/.travis/linux/build.sh | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | #!/bin/bash -ex | 1 | #!/bin/bash -ex |
| 2 | 2 | ||
| 3 | mkdir -p "$HOME/.ccache" | 3 | mkdir -p "$HOME/.ccache" |
| 4 | docker run --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh | 4 | docker run -e ENABLE_COMPATIBILITY_REPORTING --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh |
diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh index 892d2480a..4fe3326f9 100755 --- a/.travis/linux/docker.sh +++ b/.travis/linux/docker.sh | |||
| @@ -6,7 +6,7 @@ apt-get install --no-install-recommends -y build-essential git libqt5opengl5-dev | |||
| 6 | cd /yuzu | 6 | cd /yuzu |
| 7 | 7 | ||
| 8 | mkdir build && cd build | 8 | mkdir build && cd build |
| 9 | cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -G Ninja | 9 | cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -G Ninja |
| 10 | ninja | 10 | ninja |
| 11 | 11 | ||
| 12 | ccache -s | 12 | ccache -s |
diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh index e68dc1400..dce12099b 100755 --- a/.travis/macos/build.sh +++ b/.travis/macos/build.sh | |||
| @@ -9,7 +9,7 @@ export PATH="/usr/local/opt/ccache/libexec:$PATH" | |||
| 9 | 9 | ||
| 10 | mkdir build && cd build | 10 | mkdir build && cd build |
| 11 | cmake --version | 11 | cmake --version |
| 12 | cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON | 12 | cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DUSE_DISCORD_PRESENCE=ON |
| 13 | make -j4 | 13 | make -j4 |
| 14 | 14 | ||
| 15 | ccache -s | 15 | ccache -s |
diff --git a/CMakeLists.txt b/CMakeLists.txt index cd990188e..047443704 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
| @@ -15,10 +15,14 @@ CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 binaries" ON | |||
| 15 | option(ENABLE_QT "Enable the Qt frontend" ON) | 15 | option(ENABLE_QT "Enable the Qt frontend" ON) |
| 16 | CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF) | 16 | CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF) |
| 17 | 17 | ||
| 18 | option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) | ||
| 19 | |||
| 18 | option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) | 20 | option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) |
| 19 | 21 | ||
| 20 | option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) | 22 | option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) |
| 21 | 23 | ||
| 24 | option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) | ||
| 25 | |||
| 22 | if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) | 26 | if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) |
| 23 | message(STATUS "Copying pre-commit hook") | 27 | message(STATUS "Copying pre-commit hook") |
| 24 | file(COPY hooks/pre-commit | 28 | file(COPY hooks/pre-commit |
| @@ -321,6 +325,13 @@ if (ENABLE_QT) | |||
| 321 | find_package(Qt5 REQUIRED COMPONENTS Widgets OpenGL ${QT_PREFIX_HINT}) | 325 | find_package(Qt5 REQUIRED COMPONENTS Widgets OpenGL ${QT_PREFIX_HINT}) |
| 322 | endif() | 326 | endif() |
| 323 | 327 | ||
| 328 | if (ENABLE_WEB_SERVICE) | ||
| 329 | add_definitions(-DENABLE_WEB_SERVICE) | ||
| 330 | endif() | ||
| 331 | if (YUZU_ENABLE_COMPATIBILITY_REPORTING) | ||
| 332 | add_definitions(-DYUZU_ENABLE_COMPATIBILITY_REPORTING) | ||
| 333 | endif() | ||
| 334 | |||
| 324 | # Platform-specific library requirements | 335 | # Platform-specific library requirements |
| 325 | # ====================================== | 336 | # ====================================== |
| 326 | 337 | ||
diff --git a/appveyor.yml b/appveyor.yml index d475816ab..6d0e6522a 100644 --- a/appveyor.yml +++ b/appveyor.yml | |||
| @@ -39,11 +39,12 @@ before_build: | |||
| 39 | - mkdir %BUILD_TYPE%_build | 39 | - mkdir %BUILD_TYPE%_build |
| 40 | - cd %BUILD_TYPE%_build | 40 | - cd %BUILD_TYPE%_build |
| 41 | - ps: | | 41 | - ps: | |
| 42 | $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} | ||
| 42 | if ($env:BUILD_TYPE -eq 'msvc') { | 43 | if ($env:BUILD_TYPE -eq 'msvc') { |
| 43 | # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning | 44 | # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning |
| 44 | cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0' | 45 | cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DUSE_DISCORD_PRESENCE=ON .. 2>&1 && exit 0' |
| 45 | } else { | 46 | } else { |
| 46 | C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1" | 47 | C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DUSE_DISCORD_PRESENCE=ON .. 2>&1" |
| 47 | } | 48 | } |
| 48 | - cd .. | 49 | - cd .. |
| 49 | 50 | ||
diff --git a/externals/discord-rpc b/externals/discord-rpc new file mode 160000 | |||
| Subproject e32d001809c4aad56cef2a5321b54442d830174 | |||
diff --git a/externals/libressl b/externals/libressl new file mode 160000 | |||
| Subproject 7d01cb01cb1a926ecb4c9c98b107ef3c26f59df | |||
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a88551fbc..f69d00a2b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt | |||
| @@ -13,3 +13,6 @@ endif() | |||
| 13 | if (ENABLE_QT) | 13 | if (ENABLE_QT) |
| 14 | add_subdirectory(yuzu) | 14 | add_subdirectory(yuzu) |
| 15 | endif() | 15 | endif() |
| 16 | if (ENABLE_WEB_SERVICE) | ||
| 17 | add_subdirectory(web_service) | ||
| 18 | endif() | ||
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6a3f1fe08..8985e4367 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt | |||
| @@ -41,6 +41,8 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOU | |||
| 41 | add_library(common STATIC | 41 | add_library(common STATIC |
| 42 | alignment.h | 42 | alignment.h |
| 43 | assert.h | 43 | assert.h |
| 44 | detached_tasks.cpp | ||
| 45 | detached_tasks.h | ||
| 44 | bit_field.h | 46 | bit_field.h |
| 45 | bit_set.h | 47 | bit_set.h |
| 46 | cityhash.cpp | 48 | cityhash.cpp |
| @@ -87,6 +89,7 @@ add_library(common STATIC | |||
| 87 | timer.cpp | 89 | timer.cpp |
| 88 | timer.h | 90 | timer.h |
| 89 | vector_math.h | 91 | vector_math.h |
| 92 | web_result.h | ||
| 90 | ) | 93 | ) |
| 91 | 94 | ||
| 92 | if(ARCHITECTURE_x86_64) | 95 | if(ARCHITECTURE_x86_64) |
diff --git a/src/common/detached_tasks.cpp b/src/common/detached_tasks.cpp new file mode 100644 index 000000000..a347d9e02 --- /dev/null +++ b/src/common/detached_tasks.cpp | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <thread> | ||
| 6 | #include "common/assert.h" | ||
| 7 | #include "common/detached_tasks.h" | ||
| 8 | |||
| 9 | namespace Common { | ||
| 10 | |||
| 11 | DetachedTasks* DetachedTasks::instance = nullptr; | ||
| 12 | |||
| 13 | DetachedTasks::DetachedTasks() { | ||
| 14 | ASSERT(instance == nullptr); | ||
| 15 | instance = this; | ||
| 16 | } | ||
| 17 | |||
| 18 | void DetachedTasks::WaitForAllTasks() { | ||
| 19 | std::unique_lock<std::mutex> lock(mutex); | ||
| 20 | cv.wait(lock, [this]() { return count == 0; }); | ||
| 21 | } | ||
| 22 | |||
| 23 | DetachedTasks::~DetachedTasks() { | ||
| 24 | std::unique_lock<std::mutex> lock(mutex); | ||
| 25 | ASSERT(count == 0); | ||
| 26 | instance = nullptr; | ||
| 27 | } | ||
| 28 | |||
| 29 | void DetachedTasks::AddTask(std::function<void()> task) { | ||
| 30 | std::unique_lock<std::mutex> lock(instance->mutex); | ||
| 31 | ++instance->count; | ||
| 32 | std::thread([task{std::move(task)}]() { | ||
| 33 | task(); | ||
| 34 | std::unique_lock<std::mutex> lock(instance->mutex); | ||
| 35 | --instance->count; | ||
| 36 | std::notify_all_at_thread_exit(instance->cv, std::move(lock)); | ||
| 37 | }) | ||
| 38 | .detach(); | ||
| 39 | } | ||
| 40 | |||
| 41 | } // namespace Common | ||
diff --git a/src/common/detached_tasks.h b/src/common/detached_tasks.h new file mode 100644 index 000000000..eae27788d --- /dev/null +++ b/src/common/detached_tasks.h | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | #include <condition_variable> | ||
| 7 | #include <functional> | ||
| 8 | |||
| 9 | namespace Common { | ||
| 10 | |||
| 11 | /** | ||
| 12 | * A background manager which ensures that all detached task is finished before program exits. | ||
| 13 | * | ||
| 14 | * Some tasks, telemetry submission for example, prefer executing asynchronously and don't care | ||
| 15 | * about the result. These tasks are suitable for std::thread::detach(). However, this is unsafe if | ||
| 16 | * the task is launched just before the program exits (which is a common case for telemetry), so we | ||
| 17 | * need to block on these tasks on program exit. | ||
| 18 | * | ||
| 19 | * To make detached task safe, a single DetachedTasks object should be placed in the main(), and | ||
| 20 | * call WaitForAllTasks() after all program execution but before global/static variable destruction. | ||
| 21 | * Any potentially unsafe detached task should be executed via DetachedTasks::AddTask. | ||
| 22 | */ | ||
| 23 | class DetachedTasks { | ||
| 24 | public: | ||
| 25 | DetachedTasks(); | ||
| 26 | ~DetachedTasks(); | ||
| 27 | void WaitForAllTasks(); | ||
| 28 | |||
| 29 | static void AddTask(std::function<void()> task); | ||
| 30 | |||
| 31 | private: | ||
| 32 | static DetachedTasks* instance; | ||
| 33 | |||
| 34 | std::condition_variable cv; | ||
| 35 | std::mutex mutex; | ||
| 36 | int count = 0; | ||
| 37 | }; | ||
| 38 | |||
| 39 | } // namespace Common | ||
diff --git a/src/common/web_result.h b/src/common/web_result.h new file mode 100644 index 000000000..13610a7ea --- /dev/null +++ b/src/common/web_result.h | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | // Copyright 2018 yuzu Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <string> | ||
| 8 | |||
| 9 | namespace Common { | ||
| 10 | struct WebResult { | ||
| 11 | enum class Code : u32 { | ||
| 12 | Success, | ||
| 13 | InvalidURL, | ||
| 14 | CredentialsMissing, | ||
| 15 | LibError, | ||
| 16 | HttpError, | ||
| 17 | WrongContent, | ||
| 18 | NoWebservice, | ||
| 19 | }; | ||
| 20 | Code result_code; | ||
| 21 | std::string result_string; | ||
| 22 | std::string returned_data; | ||
| 23 | }; | ||
| 24 | } // namespace Commo \ No newline at end of file | ||
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 23fd6e920..95f8b5d4a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt | |||
| @@ -394,6 +394,9 @@ create_target_directory_groups(core) | |||
| 394 | 394 | ||
| 395 | target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) | 395 | target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) |
| 396 | target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives) | 396 | target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives) |
| 397 | if (ENABLE_WEB_SERVICE) | ||
| 398 | target_link_libraries(core PUBLIC json-headers web_service) | ||
| 399 | endif() | ||
| 397 | 400 | ||
| 398 | if (ARCHITECTURE_x86_64) | 401 | if (ARCHITECTURE_x86_64) |
| 399 | target_sources(core PRIVATE | 402 | target_sources(core PRIVATE |
diff --git a/src/core/settings.h b/src/core/settings.h index 0318d019c..1808f5937 100644 --- a/src/core/settings.h +++ b/src/core/settings.h | |||
| @@ -155,6 +155,12 @@ struct Values { | |||
| 155 | // Debugging | 155 | // Debugging |
| 156 | bool use_gdbstub; | 156 | bool use_gdbstub; |
| 157 | u16 gdbstub_port; | 157 | u16 gdbstub_port; |
| 158 | |||
| 159 | // WebService | ||
| 160 | bool enable_telemetry; | ||
| 161 | std::string web_api_url; | ||
| 162 | std::string yuzu_username; | ||
| 163 | std::string yuzu_token; | ||
| 158 | } extern values; | 164 | } extern values; |
| 159 | 165 | ||
| 160 | void Apply(); | 166 | void Apply(); |
diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index b0df154ca..09c85297a 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp | |||
| @@ -6,6 +6,8 @@ | |||
| 6 | #include "common/common_types.h" | 6 | #include "common/common_types.h" |
| 7 | #include "common/file_util.h" | 7 | #include "common/file_util.h" |
| 8 | 8 | ||
| 9 | #include <mbedtls/ctr_drbg.h> | ||
| 10 | #include <mbedtls/entropy.h> | ||
| 9 | #include "core/core.h" | 11 | #include "core/core.h" |
| 10 | #include "core/file_sys/control_metadata.h" | 12 | #include "core/file_sys/control_metadata.h" |
| 11 | #include "core/file_sys/patch_manager.h" | 13 | #include "core/file_sys/patch_manager.h" |
| @@ -13,10 +15,30 @@ | |||
| 13 | #include "core/settings.h" | 15 | #include "core/settings.h" |
| 14 | #include "core/telemetry_session.h" | 16 | #include "core/telemetry_session.h" |
| 15 | 17 | ||
| 18 | #ifdef ENABLE_WEB_SERVICE | ||
| 19 | #include "web_service/telemetry_json.h" | ||
| 20 | #include "web_service/verify_login.h" | ||
| 21 | #endif | ||
| 22 | |||
| 16 | namespace Core { | 23 | namespace Core { |
| 17 | 24 | ||
| 18 | static u64 GenerateTelemetryId() { | 25 | static u64 GenerateTelemetryId() { |
| 19 | u64 telemetry_id{}; | 26 | u64 telemetry_id{}; |
| 27 | |||
| 28 | mbedtls_entropy_context entropy; | ||
| 29 | mbedtls_entropy_init(&entropy); | ||
| 30 | mbedtls_ctr_drbg_context ctr_drbg; | ||
| 31 | const char* personalization = "yuzu Telemetry ID"; | ||
| 32 | |||
| 33 | mbedtls_ctr_drbg_init(&ctr_drbg); | ||
| 34 | mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, | ||
| 35 | (const unsigned char*)personalization, strlen(personalization)); | ||
| 36 | ASSERT(mbedtls_ctr_drbg_random(&ctr_drbg, reinterpret_cast<unsigned char*>(&telemetry_id), | ||
| 37 | sizeof(u64)) == 0); | ||
| 38 | |||
| 39 | mbedtls_ctr_drbg_free(&ctr_drbg); | ||
| 40 | mbedtls_entropy_free(&entropy); | ||
| 41 | |||
| 20 | return telemetry_id; | 42 | return telemetry_id; |
| 21 | } | 43 | } |
| 22 | 44 | ||
| @@ -25,14 +47,21 @@ u64 GetTelemetryId() { | |||
| 25 | const std::string filename{FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + | 47 | const std::string filename{FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + |
| 26 | "telemetry_id"}; | 48 | "telemetry_id"}; |
| 27 | 49 | ||
| 28 | if (FileUtil::Exists(filename)) { | 50 | bool generate_new_id = !FileUtil::Exists(filename); |
| 51 | if (!generate_new_id) { | ||
| 29 | FileUtil::IOFile file(filename, "rb"); | 52 | FileUtil::IOFile file(filename, "rb"); |
| 30 | if (!file.IsOpen()) { | 53 | if (!file.IsOpen()) { |
| 31 | LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); | 54 | LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); |
| 32 | return {}; | 55 | return {}; |
| 33 | } | 56 | } |
| 34 | file.ReadBytes(&telemetry_id, sizeof(u64)); | 57 | file.ReadBytes(&telemetry_id, sizeof(u64)); |
| 35 | } else { | 58 | if (telemetry_id == 0) { |
| 59 | LOG_ERROR(Frontend, "telemetry_id is 0. Generating a new one.", telemetry_id); | ||
| 60 | generate_new_id = true; | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | if (generate_new_id) { | ||
| 36 | FileUtil::IOFile file(filename, "wb"); | 65 | FileUtil::IOFile file(filename, "wb"); |
| 37 | if (!file.IsOpen()) { | 66 | if (!file.IsOpen()) { |
| 38 | LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); | 67 | LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); |
| @@ -59,23 +88,20 @@ u64 RegenerateTelemetryId() { | |||
| 59 | return new_telemetry_id; | 88 | return new_telemetry_id; |
| 60 | } | 89 | } |
| 61 | 90 | ||
| 62 | std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func) { | 91 | bool VerifyLogin(std::string username, std::string token) { |
| 63 | #ifdef ENABLE_WEB_SERVICE | 92 | #ifdef ENABLE_WEB_SERVICE |
| 64 | return WebService::VerifyLogin(username, token, Settings::values.verify_endpoint_url, func); | 93 | return WebService::VerifyLogin(Settings::values.web_api_url, username, token); |
| 65 | #else | 94 | #else |
| 66 | return std::async(std::launch::async, [func{std::move(func)}]() { | 95 | return false; |
| 67 | func(); | ||
| 68 | return false; | ||
| 69 | }); | ||
| 70 | #endif | 96 | #endif |
| 71 | } | 97 | } |
| 72 | 98 | ||
| 73 | TelemetrySession::TelemetrySession() { | 99 | TelemetrySession::TelemetrySession() { |
| 74 | #ifdef ENABLE_WEB_SERVICE | 100 | #ifdef ENABLE_WEB_SERVICE |
| 75 | if (Settings::values.enable_telemetry) { | 101 | if (Settings::values.enable_telemetry) { |
| 76 | backend = std::make_unique<WebService::TelemetryJson>( | 102 | backend = std::make_unique<WebService::TelemetryJson>(Settings::values.web_api_url, |
| 77 | Settings::values.telemetry_endpoint_url, Settings::values.yuzu_username, | 103 | Settings::values.yuzu_username, |
| 78 | Settings::values.yuzu_token); | 104 | Settings::values.yuzu_token); |
| 79 | } else { | 105 | } else { |
| 80 | backend = std::make_unique<Telemetry::NullVisitor>(); | 106 | backend = std::make_unique<Telemetry::NullVisitor>(); |
| 81 | } | 107 | } |
| @@ -94,7 +120,8 @@ TelemetrySession::TelemetrySession() { | |||
| 94 | u64 program_id{}; | 120 | u64 program_id{}; |
| 95 | const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)}; | 121 | const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)}; |
| 96 | if (res == Loader::ResultStatus::Success) { | 122 | if (res == Loader::ResultStatus::Success) { |
| 97 | AddField(Telemetry::FieldType::Session, "ProgramId", program_id); | 123 | std::string formatted_program_id{fmt::format("{:016X}", program_id)}; |
| 124 | AddField(Telemetry::FieldType::Session, "ProgramId", formatted_program_id); | ||
| 98 | 125 | ||
| 99 | std::string name; | 126 | std::string name; |
| 100 | System::GetInstance().GetAppLoader().ReadTitle(name); | 127 | System::GetInstance().GetAppLoader().ReadTitle(name); |
diff --git a/src/core/telemetry_session.h b/src/core/telemetry_session.h index dbc4f8bd4..e6976ad45 100644 --- a/src/core/telemetry_session.h +++ b/src/core/telemetry_session.h | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | 4 | ||
| 5 | #pragma once | 5 | #pragma once |
| 6 | 6 | ||
| 7 | #include <future> | ||
| 8 | #include <memory> | 7 | #include <memory> |
| 9 | #include "common/telemetry.h" | 8 | #include "common/telemetry.h" |
| 10 | 9 | ||
| @@ -31,6 +30,8 @@ public: | |||
| 31 | field_collection.AddField(type, name, std::move(value)); | 30 | field_collection.AddField(type, name, std::move(value)); |
| 32 | } | 31 | } |
| 33 | 32 | ||
| 33 | static void FinalizeAsyncJob(); | ||
| 34 | |||
| 34 | private: | 35 | private: |
| 35 | Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session | 36 | Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session |
| 36 | std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields | 37 | std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields |
| @@ -55,6 +56,6 @@ u64 RegenerateTelemetryId(); | |||
| 55 | * @param func A function that gets exectued when the verification is finished | 56 | * @param func A function that gets exectued when the verification is finished |
| 56 | * @returns Future with bool indicating whether the verification succeeded | 57 | * @returns Future with bool indicating whether the verification succeeded |
| 57 | */ | 58 | */ |
| 58 | std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func); | 59 | bool VerifyLogin(std::string username, std::string token); |
| 59 | 60 | ||
| 60 | } // namespace Core | 61 | } // namespace Core |
diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt new file mode 100644 index 000000000..ef77728c0 --- /dev/null +++ b/src/web_service/CMakeLists.txt | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | add_library(web_service STATIC | ||
| 2 | telemetry_json.cpp | ||
| 3 | telemetry_json.h | ||
| 4 | verify_login.cpp | ||
| 5 | verify_login.h | ||
| 6 | web_backend.cpp | ||
| 7 | web_backend.h | ||
| 8 | ) | ||
| 9 | |||
| 10 | create_target_directory_groups(web_service) | ||
| 11 | |||
| 12 | get_directory_property(OPENSSL_LIBS | ||
| 13 | DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl | ||
| 14 | DEFINITION OPENSSL_LIBS) | ||
| 15 | add_definitions(-DCPPHTTPLIB_OPENSSL_SUPPORT) | ||
| 16 | target_link_libraries(web_service PUBLIC common json-headers ${OPENSSL_LIBS} httplib lurlparser) | ||
diff --git a/src/web_service/json.h b/src/web_service/json.h new file mode 100644 index 000000000..88b31501e --- /dev/null +++ b/src/web_service/json.h | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | // This hack is needed to support json.hpp on platforms where the C++17 stdlib | ||
| 8 | // lacks std::string_view. See https://github.com/nlohmann/json/issues/735. | ||
| 9 | // clang-format off | ||
| 10 | #if !__has_include(<string_view>) && __has_include(<experimental/string_view>) | ||
| 11 | # include <experimental/string_view> | ||
| 12 | # define string_view experimental::string_view | ||
| 13 | # include <json.hpp> | ||
| 14 | # undef string_view | ||
| 15 | #else | ||
| 16 | # include <json.hpp> | ||
| 17 | #endif | ||
| 18 | // clang-format on | ||
diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp new file mode 100644 index 000000000..a0b7f9c4e --- /dev/null +++ b/src/web_service/telemetry_json.cpp | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <thread> | ||
| 6 | #include "common/assert.h" | ||
| 7 | #include "common/detached_tasks.h" | ||
| 8 | #include "web_service/telemetry_json.h" | ||
| 9 | #include "web_service/web_backend.h" | ||
| 10 | |||
| 11 | namespace WebService { | ||
| 12 | |||
| 13 | template <class T> | ||
| 14 | void TelemetryJson::Serialize(Telemetry::FieldType type, const std::string& name, T value) { | ||
| 15 | sections[static_cast<u8>(type)][name] = value; | ||
| 16 | } | ||
| 17 | |||
| 18 | void TelemetryJson::SerializeSection(Telemetry::FieldType type, const std::string& name) { | ||
| 19 | TopSection()[name] = sections[static_cast<unsigned>(type)]; | ||
| 20 | } | ||
| 21 | |||
| 22 | void TelemetryJson::Visit(const Telemetry::Field<bool>& field) { | ||
| 23 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 24 | } | ||
| 25 | |||
| 26 | void TelemetryJson::Visit(const Telemetry::Field<double>& field) { | ||
| 27 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 28 | } | ||
| 29 | |||
| 30 | void TelemetryJson::Visit(const Telemetry::Field<float>& field) { | ||
| 31 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 32 | } | ||
| 33 | |||
| 34 | void TelemetryJson::Visit(const Telemetry::Field<u8>& field) { | ||
| 35 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 36 | } | ||
| 37 | |||
| 38 | void TelemetryJson::Visit(const Telemetry::Field<u16>& field) { | ||
| 39 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 40 | } | ||
| 41 | |||
| 42 | void TelemetryJson::Visit(const Telemetry::Field<u32>& field) { | ||
| 43 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 44 | } | ||
| 45 | |||
| 46 | void TelemetryJson::Visit(const Telemetry::Field<u64>& field) { | ||
| 47 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 48 | } | ||
| 49 | |||
| 50 | void TelemetryJson::Visit(const Telemetry::Field<s8>& field) { | ||
| 51 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 52 | } | ||
| 53 | |||
| 54 | void TelemetryJson::Visit(const Telemetry::Field<s16>& field) { | ||
| 55 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 56 | } | ||
| 57 | |||
| 58 | void TelemetryJson::Visit(const Telemetry::Field<s32>& field) { | ||
| 59 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 60 | } | ||
| 61 | |||
| 62 | void TelemetryJson::Visit(const Telemetry::Field<s64>& field) { | ||
| 63 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 64 | } | ||
| 65 | |||
| 66 | void TelemetryJson::Visit(const Telemetry::Field<std::string>& field) { | ||
| 67 | Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||
| 68 | } | ||
| 69 | |||
| 70 | void TelemetryJson::Visit(const Telemetry::Field<const char*>& field) { | ||
| 71 | Serialize(field.GetType(), field.GetName(), std::string(field.GetValue())); | ||
| 72 | } | ||
| 73 | |||
| 74 | void TelemetryJson::Visit(const Telemetry::Field<std::chrono::microseconds>& field) { | ||
| 75 | Serialize(field.GetType(), field.GetName(), field.GetValue().count()); | ||
| 76 | } | ||
| 77 | |||
| 78 | void TelemetryJson::Complete() { | ||
| 79 | SerializeSection(Telemetry::FieldType::App, "App"); | ||
| 80 | SerializeSection(Telemetry::FieldType::Session, "Session"); | ||
| 81 | SerializeSection(Telemetry::FieldType::Performance, "Performance"); | ||
| 82 | SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback"); | ||
| 83 | SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); | ||
| 84 | SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); | ||
| 85 | |||
| 86 | auto content = TopSection().dump(); | ||
| 87 | // Send the telemetry async but don't handle the errors since they were written to the log | ||
| 88 | Common::DetachedTasks::AddTask( | ||
| 89 | [host{this->host}, username{this->username}, token{this->token}, content]() { | ||
| 90 | Client{host, username, token}.PostJson("/telemetry", content, true); | ||
| 91 | }); | ||
| 92 | } | ||
| 93 | |||
| 94 | } // namespace WebService | ||
diff --git a/src/web_service/telemetry_json.h b/src/web_service/telemetry_json.h new file mode 100644 index 000000000..9bc886538 --- /dev/null +++ b/src/web_service/telemetry_json.h | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <array> | ||
| 8 | #include <string> | ||
| 9 | #include "common/telemetry.h" | ||
| 10 | #include "common/web_result.h" | ||
| 11 | #include "web_service/json.h" | ||
| 12 | |||
| 13 | namespace WebService { | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Implementation of VisitorInterface that serialized telemetry into JSON, and submits it to the | ||
| 17 | * yuzu web service | ||
| 18 | */ | ||
| 19 | class TelemetryJson : public Telemetry::VisitorInterface { | ||
| 20 | public: | ||
| 21 | TelemetryJson(const std::string& host, const std::string& username, const std::string& token) | ||
| 22 | : host(host), username(username), token(token) {} | ||
| 23 | ~TelemetryJson() = default; | ||
| 24 | |||
| 25 | void Visit(const Telemetry::Field<bool>& field) override; | ||
| 26 | void Visit(const Telemetry::Field<double>& field) override; | ||
| 27 | void Visit(const Telemetry::Field<float>& field) override; | ||
| 28 | void Visit(const Telemetry::Field<u8>& field) override; | ||
| 29 | void Visit(const Telemetry::Field<u16>& field) override; | ||
| 30 | void Visit(const Telemetry::Field<u32>& field) override; | ||
| 31 | void Visit(const Telemetry::Field<u64>& field) override; | ||
| 32 | void Visit(const Telemetry::Field<s8>& field) override; | ||
| 33 | void Visit(const Telemetry::Field<s16>& field) override; | ||
| 34 | void Visit(const Telemetry::Field<s32>& field) override; | ||
| 35 | void Visit(const Telemetry::Field<s64>& field) override; | ||
| 36 | void Visit(const Telemetry::Field<std::string>& field) override; | ||
| 37 | void Visit(const Telemetry::Field<const char*>& field) override; | ||
| 38 | void Visit(const Telemetry::Field<std::chrono::microseconds>& field) override; | ||
| 39 | |||
| 40 | void Complete() override; | ||
| 41 | |||
| 42 | private: | ||
| 43 | nlohmann::json& TopSection() { | ||
| 44 | return sections[static_cast<u8>(Telemetry::FieldType::None)]; | ||
| 45 | } | ||
| 46 | |||
| 47 | template <class T> | ||
| 48 | void Serialize(Telemetry::FieldType type, const std::string& name, T value); | ||
| 49 | |||
| 50 | void SerializeSection(Telemetry::FieldType type, const std::string& name); | ||
| 51 | |||
| 52 | nlohmann::json output; | ||
| 53 | std::array<nlohmann::json, 7> sections; | ||
| 54 | std::string host; | ||
| 55 | std::string username; | ||
| 56 | std::string token; | ||
| 57 | }; | ||
| 58 | |||
| 59 | } // namespace WebService | ||
diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp new file mode 100644 index 000000000..02e1b74f3 --- /dev/null +++ b/src/web_service/verify_login.cpp | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include "web_service/json.h" | ||
| 6 | #include "web_service/verify_login.h" | ||
| 7 | #include "web_service/web_backend.h" | ||
| 8 | |||
| 9 | namespace WebService { | ||
| 10 | |||
| 11 | bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) { | ||
| 12 | Client client(host, username, token); | ||
| 13 | auto reply = client.GetJson("/profile", false).returned_data; | ||
| 14 | if (reply.empty()) { | ||
| 15 | return false; | ||
| 16 | } | ||
| 17 | nlohmann::json json = nlohmann::json::parse(reply); | ||
| 18 | const auto iter = json.find("username"); | ||
| 19 | |||
| 20 | if (iter == json.end()) { | ||
| 21 | return username.empty(); | ||
| 22 | } | ||
| 23 | |||
| 24 | return username == *iter; | ||
| 25 | } | ||
| 26 | |||
| 27 | } // namespace WebService | ||
diff --git a/src/web_service/verify_login.h b/src/web_service/verify_login.h new file mode 100644 index 000000000..39db32dbb --- /dev/null +++ b/src/web_service/verify_login.h | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <functional> | ||
| 8 | #include <future> | ||
| 9 | #include <string> | ||
| 10 | |||
| 11 | namespace WebService { | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Checks if username and token is valid | ||
| 15 | * @param host the web API URL | ||
| 16 | * @param username yuzu username to use for authentication. | ||
| 17 | * @param token yuzu token to use for authentication. | ||
| 18 | * @returns a bool indicating whether the verification succeeded | ||
| 19 | */ | ||
| 20 | bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token); | ||
| 21 | |||
| 22 | } // namespace WebService | ||
diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp new file mode 100644 index 000000000..a726fb8eb --- /dev/null +++ b/src/web_service/web_backend.cpp | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <cstdlib> | ||
| 6 | #include <string> | ||
| 7 | #include <thread> | ||
| 8 | #include <LUrlParser.h> | ||
| 9 | #include "common/logging/log.h" | ||
| 10 | #include "common/web_result.h" | ||
| 11 | #include "core/settings.h" | ||
| 12 | #include "web_service/web_backend.h" | ||
| 13 | |||
| 14 | namespace WebService { | ||
| 15 | |||
| 16 | static constexpr char API_VERSION[]{"1"}; | ||
| 17 | |||
| 18 | constexpr int HTTP_PORT = 80; | ||
| 19 | constexpr int HTTPS_PORT = 443; | ||
| 20 | |||
| 21 | constexpr int TIMEOUT_SECONDS = 30; | ||
| 22 | |||
| 23 | Client::JWTCache Client::jwt_cache{}; | ||
| 24 | |||
| 25 | Client::Client(const std::string& host, const std::string& username, const std::string& token) | ||
| 26 | : host(host), username(username), token(token) { | ||
| 27 | if (username == jwt_cache.username && token == jwt_cache.token) { | ||
| 28 | jwt = jwt_cache.jwt; | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, | ||
| 33 | const std::string& data, const std::string& jwt, | ||
| 34 | const std::string& username, const std::string& token) { | ||
| 35 | if (cli == nullptr) { | ||
| 36 | auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); | ||
| 37 | int port; | ||
| 38 | if (parsedUrl.m_Scheme == "http") { | ||
| 39 | if (!parsedUrl.GetPort(&port)) { | ||
| 40 | port = HTTP_PORT; | ||
| 41 | } | ||
| 42 | cli = | ||
| 43 | std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS); | ||
| 44 | } else if (parsedUrl.m_Scheme == "https") { | ||
| 45 | if (!parsedUrl.GetPort(&port)) { | ||
| 46 | port = HTTPS_PORT; | ||
| 47 | } | ||
| 48 | cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port, | ||
| 49 | TIMEOUT_SECONDS); | ||
| 50 | } else { | ||
| 51 | LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme); | ||
| 52 | return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"}; | ||
| 53 | } | ||
| 54 | } | ||
| 55 | if (cli == nullptr) { | ||
| 56 | LOG_ERROR(WebService, "Invalid URL {}", host + path); | ||
| 57 | return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; | ||
| 58 | } | ||
| 59 | |||
| 60 | httplib::Headers params; | ||
| 61 | if (!jwt.empty()) { | ||
| 62 | params = { | ||
| 63 | {std::string("Authorization"), fmt::format("Bearer {}", jwt)}, | ||
| 64 | }; | ||
| 65 | } else if (!username.empty()) { | ||
| 66 | params = { | ||
| 67 | {std::string("x-username"), username}, | ||
| 68 | {std::string("x-token"), token}, | ||
| 69 | }; | ||
| 70 | } | ||
| 71 | |||
| 72 | params.emplace(std::string("api-version"), std::string(API_VERSION)); | ||
| 73 | if (method != "GET") { | ||
| 74 | params.emplace(std::string("Content-Type"), std::string("application/json")); | ||
| 75 | }; | ||
| 76 | |||
| 77 | httplib::Request request; | ||
| 78 | request.method = method; | ||
| 79 | request.path = path; | ||
| 80 | request.headers = params; | ||
| 81 | request.body = data; | ||
| 82 | |||
| 83 | httplib::Response response; | ||
| 84 | |||
| 85 | if (!cli->send(request, response)) { | ||
| 86 | LOG_ERROR(WebService, "{} to {} returned null", method, host + path); | ||
| 87 | return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; | ||
| 88 | } | ||
| 89 | |||
| 90 | if (response.status >= 400) { | ||
| 91 | LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, | ||
| 92 | response.status); | ||
| 93 | return Common::WebResult{Common::WebResult::Code::HttpError, | ||
| 94 | std::to_string(response.status)}; | ||
| 95 | } | ||
| 96 | |||
| 97 | auto content_type = response.headers.find("content-type"); | ||
| 98 | |||
| 99 | if (content_type == response.headers.end()) { | ||
| 100 | LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); | ||
| 101 | return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; | ||
| 102 | } | ||
| 103 | |||
| 104 | if (content_type->second.find("application/json") == std::string::npos && | ||
| 105 | content_type->second.find("text/html; charset=utf-8") == std::string::npos) { | ||
| 106 | LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, | ||
| 107 | content_type->second); | ||
| 108 | return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; | ||
| 109 | } | ||
| 110 | return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; | ||
| 111 | } | ||
| 112 | |||
| 113 | void Client::UpdateJWT() { | ||
| 114 | if (!username.empty() && !token.empty()) { | ||
| 115 | auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); | ||
| 116 | if (result.result_code != Common::WebResult::Code::Success) { | ||
| 117 | LOG_ERROR(WebService, "UpdateJWT failed"); | ||
| 118 | } else { | ||
| 119 | jwt_cache.username = username; | ||
| 120 | jwt_cache.token = token; | ||
| 121 | jwt_cache.jwt = jwt = result.returned_data; | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, | ||
| 127 | const std::string& data, bool allow_anonymous) { | ||
| 128 | if (jwt.empty()) { | ||
| 129 | UpdateJWT(); | ||
| 130 | } | ||
| 131 | |||
| 132 | if (jwt.empty() && !allow_anonymous) { | ||
| 133 | LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||
| 134 | return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"}; | ||
| 135 | } | ||
| 136 | |||
| 137 | auto result = GenericJson(method, path, data, jwt); | ||
| 138 | if (result.result_string == "401") { | ||
| 139 | // Try again with new JWT | ||
| 140 | UpdateJWT(); | ||
| 141 | result = GenericJson(method, path, data, jwt); | ||
| 142 | } | ||
| 143 | |||
| 144 | return result; | ||
| 145 | } | ||
| 146 | |||
| 147 | } // namespace WebService | ||
diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h new file mode 100644 index 000000000..549bcce29 --- /dev/null +++ b/src/web_service/web_backend.h | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <functional> | ||
| 8 | #include <future> | ||
| 9 | #include <string> | ||
| 10 | #include <tuple> | ||
| 11 | #include <httplib.h> | ||
| 12 | #include "common/common_types.h" | ||
| 13 | #include "common/web_result.h" | ||
| 14 | |||
| 15 | namespace httplib { | ||
| 16 | class Client; | ||
| 17 | } | ||
| 18 | |||
| 19 | namespace WebService { | ||
| 20 | |||
| 21 | class Client { | ||
| 22 | public: | ||
| 23 | Client(const std::string& host, const std::string& username, const std::string& token); | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Posts JSON to the specified path. | ||
| 27 | * @param path the URL segment after the host address. | ||
| 28 | * @param data String of JSON data to use for the body of the POST request. | ||
| 29 | * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||
| 30 | * @return the result of the request. | ||
| 31 | */ | ||
| 32 | Common::WebResult PostJson(const std::string& path, const std::string& data, | ||
| 33 | bool allow_anonymous) { | ||
| 34 | return GenericJson("POST", path, data, allow_anonymous); | ||
| 35 | } | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Gets JSON from the specified path. | ||
| 39 | * @param path the URL segment after the host address. | ||
| 40 | * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||
| 41 | * @return the result of the request. | ||
| 42 | */ | ||
| 43 | Common::WebResult GetJson(const std::string& path, bool allow_anonymous) { | ||
| 44 | return GenericJson("GET", path, "", allow_anonymous); | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Deletes JSON to the specified path. | ||
| 49 | * @param path the URL segment after the host address. | ||
| 50 | * @param data String of JSON data to use for the body of the DELETE request. | ||
| 51 | * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||
| 52 | * @return the result of the request. | ||
| 53 | */ | ||
| 54 | Common::WebResult DeleteJson(const std::string& path, const std::string& data, | ||
| 55 | bool allow_anonymous) { | ||
| 56 | return GenericJson("DELETE", path, data, allow_anonymous); | ||
| 57 | } | ||
| 58 | |||
| 59 | private: | ||
| 60 | /// A generic function handles POST, GET and DELETE request together | ||
| 61 | Common::WebResult GenericJson(const std::string& method, const std::string& path, | ||
| 62 | const std::string& data, bool allow_anonymous); | ||
| 63 | |||
| 64 | /** | ||
| 65 | * A generic function with explicit authentication method specified | ||
| 66 | * JWT is used if the jwt parameter is not empty | ||
| 67 | * username + token is used if jwt is empty but username and token are not empty | ||
| 68 | * anonymous if all of jwt, username and token are empty | ||
| 69 | */ | ||
| 70 | Common::WebResult GenericJson(const std::string& method, const std::string& path, | ||
| 71 | const std::string& data, const std::string& jwt = "", | ||
| 72 | const std::string& username = "", const std::string& token = ""); | ||
| 73 | |||
| 74 | // Retrieve a new JWT from given username and token | ||
| 75 | void UpdateJWT(); | ||
| 76 | |||
| 77 | std::string host; | ||
| 78 | std::string username; | ||
| 79 | std::string token; | ||
| 80 | std::string jwt; | ||
| 81 | std::unique_ptr<httplib::Client> cli; | ||
| 82 | |||
| 83 | struct JWTCache { | ||
| 84 | std::string username; | ||
| 85 | std::string token; | ||
| 86 | std::string jwt; | ||
| 87 | }; | ||
| 88 | static JWTCache jwt_cache; | ||
| 89 | }; | ||
| 90 | |||
| 91 | } // namespace WebService | ||
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index f48b69809..f93ba2569 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt | |||
| @@ -29,6 +29,8 @@ add_executable(yuzu | |||
| 29 | configuration/configure_input.h | 29 | configuration/configure_input.h |
| 30 | configuration/configure_system.cpp | 30 | configuration/configure_system.cpp |
| 31 | configuration/configure_system.h | 31 | configuration/configure_system.h |
| 32 | configuration/configure_web.cpp | ||
| 33 | configuration/configure_web.h | ||
| 32 | debugger/graphics/graphics_breakpoint_observer.cpp | 34 | debugger/graphics/graphics_breakpoint_observer.cpp |
| 33 | debugger/graphics/graphics_breakpoint_observer.h | 35 | debugger/graphics/graphics_breakpoint_observer.h |
| 34 | debugger/graphics/graphics_breakpoints.cpp | 36 | debugger/graphics/graphics_breakpoints.cpp |
| @@ -42,6 +44,7 @@ add_executable(yuzu | |||
| 42 | debugger/profiler.h | 44 | debugger/profiler.h |
| 43 | debugger/wait_tree.cpp | 45 | debugger/wait_tree.cpp |
| 44 | debugger/wait_tree.h | 46 | debugger/wait_tree.h |
| 47 | discord.h | ||
| 45 | game_list.cpp | 48 | game_list.cpp |
| 46 | game_list.h | 49 | game_list.h |
| 47 | game_list_p.h | 50 | game_list_p.h |
| @@ -57,6 +60,8 @@ add_executable(yuzu | |||
| 57 | util/spinbox.h | 60 | util/spinbox.h |
| 58 | util/util.cpp | 61 | util/util.cpp |
| 59 | util/util.h | 62 | util/util.h |
| 63 | compatdb.cpp | ||
| 64 | compatdb.h | ||
| 60 | yuzu.rc | 65 | yuzu.rc |
| 61 | ) | 66 | ) |
| 62 | 67 | ||
| @@ -70,8 +75,10 @@ set(UIS | |||
| 70 | configuration/configure_graphics.ui | 75 | configuration/configure_graphics.ui |
| 71 | configuration/configure_input.ui | 76 | configuration/configure_input.ui |
| 72 | configuration/configure_system.ui | 77 | configuration/configure_system.ui |
| 78 | configuration/configure_web.ui | ||
| 73 | hotkeys.ui | 79 | hotkeys.ui |
| 74 | main.ui | 80 | main.ui |
| 81 | compatdb.ui | ||
| 75 | ) | 82 | ) |
| 76 | 83 | ||
| 77 | file(GLOB COMPAT_LIST | 84 | file(GLOB COMPAT_LIST |
| @@ -113,6 +120,15 @@ target_link_libraries(yuzu PRIVATE common core input_common video_core) | |||
| 113 | target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets) | 120 | target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets) |
| 114 | target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) | 121 | target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) |
| 115 | 122 | ||
| 123 | if (USE_DISCORD_PRESENCE) | ||
| 124 | target_sources(yuzu PUBLIC | ||
| 125 | discord_impl.cpp | ||
| 126 | discord_impl.h | ||
| 127 | ) | ||
| 128 | target_link_libraries(yuzu PRIVATE discord-rpc) | ||
| 129 | target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) | ||
| 130 | endif() | ||
| 131 | |||
| 116 | if(UNIX AND NOT APPLE) | 132 | if(UNIX AND NOT APPLE) |
| 117 | install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") | 133 | install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") |
| 118 | endif() | 134 | endif() |
diff --git a/src/yuzu/compatdb.cpp b/src/yuzu/compatdb.cpp new file mode 100644 index 000000000..45f8b4461 --- /dev/null +++ b/src/yuzu/compatdb.cpp | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <QButtonGroup> | ||
| 6 | #include <QMessageBox> | ||
| 7 | #include <QPushButton> | ||
| 8 | #include "common/logging/log.h" | ||
| 9 | #include "common/telemetry.h" | ||
| 10 | #include "core/core.h" | ||
| 11 | #include "core/telemetry_session.h" | ||
| 12 | #include "ui_compatdb.h" | ||
| 13 | #include "yuzu/compatdb.h" | ||
| 14 | |||
| 15 | CompatDB::CompatDB(QWidget* parent) | ||
| 16 | : QWizard(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), | ||
| 17 | ui{std::make_unique<Ui::CompatDB>()} { | ||
| 18 | ui->setupUi(this); | ||
| 19 | connect(ui->radioButton_Perfect, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 20 | connect(ui->radioButton_Great, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 21 | connect(ui->radioButton_Okay, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 22 | connect(ui->radioButton_Bad, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 23 | connect(ui->radioButton_IntroMenu, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 24 | connect(ui->radioButton_WontBoot, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||
| 25 | connect(button(NextButton), &QPushButton::clicked, this, &CompatDB::Submit); | ||
| 26 | } | ||
| 27 | |||
| 28 | CompatDB::~CompatDB() = default; | ||
| 29 | |||
| 30 | enum class CompatDBPage { Intro = 0, Selection = 1, Final = 2 }; | ||
| 31 | |||
| 32 | void CompatDB::Submit() { | ||
| 33 | QButtonGroup* compatibility = new QButtonGroup(this); | ||
| 34 | compatibility->addButton(ui->radioButton_Perfect, 0); | ||
| 35 | compatibility->addButton(ui->radioButton_Great, 1); | ||
| 36 | compatibility->addButton(ui->radioButton_Okay, 2); | ||
| 37 | compatibility->addButton(ui->radioButton_Bad, 3); | ||
| 38 | compatibility->addButton(ui->radioButton_IntroMenu, 4); | ||
| 39 | compatibility->addButton(ui->radioButton_WontBoot, 5); | ||
| 40 | switch ((static_cast<CompatDBPage>(currentId()))) { | ||
| 41 | case CompatDBPage::Selection: | ||
| 42 | if (compatibility->checkedId() == -1) { | ||
| 43 | button(NextButton)->setEnabled(false); | ||
| 44 | } | ||
| 45 | break; | ||
| 46 | case CompatDBPage::Final: | ||
| 47 | LOG_DEBUG(Frontend, "Compatibility Rating: {}", compatibility->checkedId()); | ||
| 48 | Core::Telemetry().AddField(Telemetry::FieldType::UserFeedback, "Compatibility", | ||
| 49 | compatibility->checkedId()); | ||
| 50 | // older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a | ||
| 51 | // workaround | ||
| 52 | button(QWizard::CancelButton)->setVisible(false); | ||
| 53 | break; | ||
| 54 | default: | ||
| 55 | LOG_ERROR(Frontend, "Unexpected page: {}", currentId()); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | void CompatDB::EnableNext() { | ||
| 60 | button(NextButton)->setEnabled(true); | ||
| 61 | } | ||
diff --git a/src/yuzu/compatdb.h b/src/yuzu/compatdb.h new file mode 100644 index 000000000..0a0f27cca --- /dev/null +++ b/src/yuzu/compatdb.h | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <memory> | ||
| 8 | #include <QWizard> | ||
| 9 | |||
| 10 | namespace Ui { | ||
| 11 | class CompatDB; | ||
| 12 | } | ||
| 13 | |||
| 14 | class CompatDB : public QWizard { | ||
| 15 | Q_OBJECT | ||
| 16 | |||
| 17 | public: | ||
| 18 | explicit CompatDB(QWidget* parent = nullptr); | ||
| 19 | ~CompatDB(); | ||
| 20 | |||
| 21 | private: | ||
| 22 | std::unique_ptr<Ui::CompatDB> ui; | ||
| 23 | |||
| 24 | private slots: | ||
| 25 | void Submit(); | ||
| 26 | void EnableNext(); | ||
| 27 | }; | ||
diff --git a/src/yuzu/compatdb.ui b/src/yuzu/compatdb.ui new file mode 100644 index 000000000..fed402176 --- /dev/null +++ b/src/yuzu/compatdb.ui | |||
| @@ -0,0 +1,215 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <ui version="4.0"> | ||
| 3 | <class>CompatDB</class> | ||
| 4 | <widget class="QWizard" name="CompatDB"> | ||
| 5 | <property name="geometry"> | ||
| 6 | <rect> | ||
| 7 | <x>0</x> | ||
| 8 | <y>0</y> | ||
| 9 | <width>600</width> | ||
| 10 | <height>482</height> | ||
| 11 | </rect> | ||
| 12 | </property> | ||
| 13 | <property name="minimumSize"> | ||
| 14 | <size> | ||
| 15 | <width>500</width> | ||
| 16 | <height>410</height> | ||
| 17 | </size> | ||
| 18 | </property> | ||
| 19 | <property name="windowTitle"> | ||
| 20 | <string>Report Compatibility</string> | ||
| 21 | </property> | ||
| 22 | <property name="options"> | ||
| 23 | <set>QWizard::DisabledBackButtonOnLastPage|QWizard::HelpButtonOnRight|QWizard::NoBackButtonOnStartPage</set> | ||
| 24 | </property> | ||
| 25 | <widget class="QWizardPage" name="wizard_Info"> | ||
| 26 | <property name="title"> | ||
| 27 | <string>Report Game Compatibility</string> | ||
| 28 | </property> | ||
| 29 | <attribute name="pageId"> | ||
| 30 | <string notr="true">0</string> | ||
| 31 | </attribute> | ||
| 32 | <layout class="QVBoxLayout" name="verticalLayout"> | ||
| 33 | <item> | ||
| 34 | <widget class="QLabel" name="lbl_Spiel"> | ||
| 35 | <property name="text"> | ||
| 36 | <string><html><head/><body><p><span style=" font-size:10pt;">Should you choose to submit a test case to the </span><a href="https://yuzu-emu.org/game/"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">yuzu Compatibility List</span></a><span style=" font-size:10pt;">, The following information will be collected and displayed on the site:</span></p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Hardware Information (CPU / GPU / Operating System)</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Which version of yuzu you are running</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The connected yuzu account</li></ul></body></html></string> | ||
| 37 | </property> | ||
| 38 | <property name="wordWrap"> | ||
| 39 | <bool>true</bool> | ||
| 40 | </property> | ||
| 41 | <property name="openExternalLinks"> | ||
| 42 | <bool>true</bool> | ||
| 43 | </property> | ||
| 44 | </widget> | ||
| 45 | </item> | ||
| 46 | <item> | ||
| 47 | <spacer name="verticalSpacer_2"> | ||
| 48 | <property name="orientation"> | ||
| 49 | <enum>Qt::Vertical</enum> | ||
| 50 | </property> | ||
| 51 | <property name="sizeHint" stdset="0"> | ||
| 52 | <size> | ||
| 53 | <width>20</width> | ||
| 54 | <height>0</height> | ||
| 55 | </size> | ||
| 56 | </property> | ||
| 57 | </spacer> | ||
| 58 | </item> | ||
| 59 | </layout> | ||
| 60 | </widget> | ||
| 61 | <widget class="QWizardPage" name="wizard_Report"> | ||
| 62 | <property name="title"> | ||
| 63 | <string>Report Game Compatibility</string> | ||
| 64 | </property> | ||
| 65 | <attribute name="pageId"> | ||
| 66 | <string notr="true">1</string> | ||
| 67 | </attribute> | ||
| 68 | <layout class="QFormLayout" name="formLayout"> | ||
| 69 | <item row="2" column="0"> | ||
| 70 | <widget class="QRadioButton" name="radioButton_Perfect"> | ||
| 71 | <property name="text"> | ||
| 72 | <string>Perfect</string> | ||
| 73 | </property> | ||
| 74 | </widget> | ||
| 75 | </item> | ||
| 76 | <item row="2" column="1"> | ||
| 77 | <widget class="QLabel" name="lbl_Perfect"> | ||
| 78 | <property name="text"> | ||
| 79 | <string><html><head/><body><p>Game functions flawlessly with no audio or graphical glitches.</p></body></html></string> | ||
| 80 | </property> | ||
| 81 | <property name="wordWrap"> | ||
| 82 | <bool>true</bool> | ||
| 83 | </property> | ||
| 84 | </widget> | ||
| 85 | </item> | ||
| 86 | <item row="4" column="0"> | ||
| 87 | <widget class="QRadioButton" name="radioButton_Great"> | ||
| 88 | <property name="text"> | ||
| 89 | <string>Great </string> | ||
| 90 | </property> | ||
| 91 | </widget> | ||
| 92 | </item> | ||
| 93 | <item row="4" column="1"> | ||
| 94 | <widget class="QLabel" name="lbl_Great"> | ||
| 95 | <property name="text"> | ||
| 96 | <string><html><head/><body><p>Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.</p></body></html></string> | ||
| 97 | </property> | ||
| 98 | <property name="wordWrap"> | ||
| 99 | <bool>true</bool> | ||
| 100 | </property> | ||
| 101 | </widget> | ||
| 102 | </item> | ||
| 103 | <item row="5" column="0"> | ||
| 104 | <widget class="QRadioButton" name="radioButton_Okay"> | ||
| 105 | <property name="text"> | ||
| 106 | <string>Okay</string> | ||
| 107 | </property> | ||
| 108 | </widget> | ||
| 109 | </item> | ||
| 110 | <item row="5" column="1"> | ||
| 111 | <widget class="QLabel" name="lbl_Okay"> | ||
| 112 | <property name="text"> | ||
| 113 | <string><html><head/><body><p>Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.</p></body></html></string> | ||
| 114 | </property> | ||
| 115 | <property name="wordWrap"> | ||
| 116 | <bool>true</bool> | ||
| 117 | </property> | ||
| 118 | </widget> | ||
| 119 | </item> | ||
| 120 | <item row="6" column="0"> | ||
| 121 | <widget class="QRadioButton" name="radioButton_Bad"> | ||
| 122 | <property name="text"> | ||
| 123 | <string>Bad</string> | ||
| 124 | </property> | ||
| 125 | </widget> | ||
| 126 | </item> | ||
| 127 | <item row="6" column="1"> | ||
| 128 | <widget class="QLabel" name="lbl_Bad"> | ||
| 129 | <property name="text"> | ||
| 130 | <string><html><head/><body><p>Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.</p></body></html></string> | ||
| 131 | </property> | ||
| 132 | <property name="wordWrap"> | ||
| 133 | <bool>true</bool> | ||
| 134 | </property> | ||
| 135 | </widget> | ||
| 136 | </item> | ||
| 137 | <item row="7" column="0"> | ||
| 138 | <widget class="QRadioButton" name="radioButton_IntroMenu"> | ||
| 139 | <property name="text"> | ||
| 140 | <string>Intro/Menu</string> | ||
| 141 | </property> | ||
| 142 | </widget> | ||
| 143 | </item> | ||
| 144 | <item row="7" column="1"> | ||
| 145 | <widget class="QLabel" name="lbl_IntroMenu"> | ||
| 146 | <property name="text"> | ||
| 147 | <string><html><head/><body><p>Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.</p></body></html></string> | ||
| 148 | </property> | ||
| 149 | <property name="wordWrap"> | ||
| 150 | <bool>true</bool> | ||
| 151 | </property> | ||
| 152 | </widget> | ||
| 153 | </item> | ||
| 154 | <item row="8" column="0"> | ||
| 155 | <widget class="QRadioButton" name="radioButton_WontBoot"> | ||
| 156 | <property name="text"> | ||
| 157 | <string>Won't Boot</string> | ||
| 158 | </property> | ||
| 159 | <property name="checkable"> | ||
| 160 | <bool>true</bool> | ||
| 161 | </property> | ||
| 162 | <property name="checked"> | ||
| 163 | <bool>false</bool> | ||
| 164 | </property> | ||
| 165 | </widget> | ||
| 166 | </item> | ||
| 167 | <item row="8" column="1"> | ||
| 168 | <widget class="QLabel" name="lbl_WontBoot"> | ||
| 169 | <property name="text"> | ||
| 170 | <string><html><head/><body><p>The game crashes when attempting to startup.</p></body></html></string> | ||
| 171 | </property> | ||
| 172 | </widget> | ||
| 173 | </item> | ||
| 174 | <item row="0" column="0" colspan="2"> | ||
| 175 | <widget class="QLabel" name="lbl_Independent"> | ||
| 176 | <property name="font"> | ||
| 177 | <font> | ||
| 178 | <pointsize>10</pointsize> | ||
| 179 | </font> | ||
| 180 | </property> | ||
| 181 | <property name="text"> | ||
| 182 | <string><html><head/><body><p>Independent of speed or performance, how well does this game play from start to finish on this version of yuzu?</p></body></html></string> | ||
| 183 | </property> | ||
| 184 | <property name="wordWrap"> | ||
| 185 | <bool>true</bool> | ||
| 186 | </property> | ||
| 187 | </widget> | ||
| 188 | </item> | ||
| 189 | <item row="1" column="0" colspan="2"> | ||
| 190 | <spacer name="verticalSpacer"> | ||
| 191 | <property name="orientation"> | ||
| 192 | <enum>Qt::Vertical</enum> | ||
| 193 | </property> | ||
| 194 | <property name="sizeHint" stdset="0"> | ||
| 195 | <size> | ||
| 196 | <width>20</width> | ||
| 197 | <height>0</height> | ||
| 198 | </size> | ||
| 199 | </property> | ||
| 200 | </spacer> | ||
| 201 | </item> | ||
| 202 | </layout> | ||
| 203 | </widget> | ||
| 204 | <widget class="QWizardPage" name="wizard_ThankYou"> | ||
| 205 | <property name="title"> | ||
| 206 | <string>Thank you for your submission!</string> | ||
| 207 | </property> | ||
| 208 | <attribute name="pageId"> | ||
| 209 | <string notr="true">2</string> | ||
| 210 | </attribute> | ||
| 211 | </widget> | ||
| 212 | </widget> | ||
| 213 | <resources/> | ||
| 214 | <connections/> | ||
| 215 | </ui> | ||
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index d229225b4..650dd03c0 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp | |||
| @@ -136,8 +136,18 @@ void Config::ReadValues() { | |||
| 136 | Settings::values.gdbstub_port = qt_config->value("gdbstub_port", 24689).toInt(); | 136 | Settings::values.gdbstub_port = qt_config->value("gdbstub_port", 24689).toInt(); |
| 137 | qt_config->endGroup(); | 137 | qt_config->endGroup(); |
| 138 | 138 | ||
| 139 | qt_config->beginGroup("WebService"); | ||
| 140 | Settings::values.enable_telemetry = qt_config->value("enable_telemetry", true).toBool(); | ||
| 141 | Settings::values.web_api_url = | ||
| 142 | qt_config->value("web_api_url", "https://api.yuzu-emu.org").toString().toStdString(); | ||
| 143 | Settings::values.yuzu_username = qt_config->value("yuzu_username").toString().toStdString(); | ||
| 144 | Settings::values.yuzu_token = qt_config->value("yuzu_token").toString().toStdString(); | ||
| 145 | qt_config->endGroup(); | ||
| 146 | |||
| 139 | qt_config->beginGroup("UI"); | 147 | qt_config->beginGroup("UI"); |
| 140 | UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); | 148 | UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); |
| 149 | UISettings::values.enable_discord_presence = | ||
| 150 | qt_config->value("enable_discord_presence", true).toBool(); | ||
| 141 | 151 | ||
| 142 | qt_config->beginGroup("UIGameList"); | 152 | qt_config->beginGroup("UIGameList"); |
| 143 | UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool(); | 153 | UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool(); |
| @@ -261,8 +271,16 @@ void Config::SaveValues() { | |||
| 261 | qt_config->setValue("gdbstub_port", Settings::values.gdbstub_port); | 271 | qt_config->setValue("gdbstub_port", Settings::values.gdbstub_port); |
| 262 | qt_config->endGroup(); | 272 | qt_config->endGroup(); |
| 263 | 273 | ||
| 274 | qt_config->beginGroup("WebService"); | ||
| 275 | qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry); | ||
| 276 | qt_config->setValue("web_api_url", QString::fromStdString(Settings::values.web_api_url)); | ||
| 277 | qt_config->setValue("yuzu_username", QString::fromStdString(Settings::values.yuzu_username)); | ||
| 278 | qt_config->setValue("yuzu_token", QString::fromStdString(Settings::values.yuzu_token)); | ||
| 279 | qt_config->endGroup(); | ||
| 280 | |||
| 264 | qt_config->beginGroup("UI"); | 281 | qt_config->beginGroup("UI"); |
| 265 | qt_config->setValue("theme", UISettings::values.theme); | 282 | qt_config->setValue("theme", UISettings::values.theme); |
| 283 | qt_config->setValue("enable_discord_presence", UISettings::values.enable_discord_presence); | ||
| 266 | 284 | ||
| 267 | qt_config->beginGroup("UIGameList"); | 285 | qt_config->beginGroup("UIGameList"); |
| 268 | qt_config->setValue("show_unknown", UISettings::values.show_unknown); | 286 | qt_config->setValue("show_unknown", UISettings::values.show_unknown); |
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui index 20f120134..9b297df28 100644 --- a/src/yuzu/configuration/configure.ui +++ b/src/yuzu/configuration/configure.ui | |||
| @@ -54,6 +54,11 @@ | |||
| 54 | <string>Debug</string> | 54 | <string>Debug</string> |
| 55 | </attribute> | 55 | </attribute> |
| 56 | </widget> | 56 | </widget> |
| 57 | <widget class="ConfigureWeb" name="webTab"> | ||
| 58 | <attribute name="title"> | ||
| 59 | <string>Web</string> | ||
| 60 | </attribute> | ||
| 61 | </widget> | ||
| 57 | </widget> | 62 | </widget> |
| 58 | </item> | 63 | </item> |
| 59 | <item> | 64 | <item> |
| @@ -108,6 +113,12 @@ | |||
| 108 | <header>configuration/configure_graphics.h</header> | 113 | <header>configuration/configure_graphics.h</header> |
| 109 | <container>1</container> | 114 | <container>1</container> |
| 110 | </customwidget> | 115 | </customwidget> |
| 116 | <customwidget> | ||
| 117 | <class>ConfigureWeb</class> | ||
| 118 | <extends>QWidget</extends> | ||
| 119 | <header>configuration/configure_web.h</header> | ||
| 120 | <container>1</container> | ||
| 121 | </customwidget> | ||
| 111 | </customwidgets> | 122 | </customwidgets> |
| 112 | <resources/> | 123 | <resources/> |
| 113 | <connections> | 124 | <connections> |
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index daa4cc0d9..3905423e9 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp | |||
| @@ -27,5 +27,6 @@ void ConfigureDialog::applyConfiguration() { | |||
| 27 | ui->graphicsTab->applyConfiguration(); | 27 | ui->graphicsTab->applyConfiguration(); |
| 28 | ui->audioTab->applyConfiguration(); | 28 | ui->audioTab->applyConfiguration(); |
| 29 | ui->debugTab->applyConfiguration(); | 29 | ui->debugTab->applyConfiguration(); |
| 30 | ui->webTab->applyConfiguration(); | ||
| 30 | Settings::Apply(); | 31 | Settings::Apply(); |
| 31 | } | 32 | } |
diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp new file mode 100644 index 000000000..cfca08014 --- /dev/null +++ b/src/yuzu/configuration/configure_web.cpp | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <QIcon> | ||
| 6 | #include <QMessageBox> | ||
| 7 | #include <QtConcurrent/QtConcurrentRun> | ||
| 8 | #include "core/settings.h" | ||
| 9 | #include "core/telemetry_session.h" | ||
| 10 | #include "ui_configure_web.h" | ||
| 11 | #include "yuzu/configuration/configure_web.h" | ||
| 12 | #include "yuzu/ui_settings.h" | ||
| 13 | |||
| 14 | ConfigureWeb::ConfigureWeb(QWidget* parent) | ||
| 15 | : QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { | ||
| 16 | ui->setupUi(this); | ||
| 17 | connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this, | ||
| 18 | &ConfigureWeb::RefreshTelemetryID); | ||
| 19 | connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin); | ||
| 20 | connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified); | ||
| 21 | |||
| 22 | #ifndef USE_DISCORD_PRESENCE | ||
| 23 | ui->discord_group->setVisible(false); | ||
| 24 | #endif | ||
| 25 | this->setConfiguration(); | ||
| 26 | } | ||
| 27 | |||
| 28 | ConfigureWeb::~ConfigureWeb() {} | ||
| 29 | |||
| 30 | void ConfigureWeb::setConfiguration() { | ||
| 31 | ui->web_credentials_disclaimer->setWordWrap(true); | ||
| 32 | ui->telemetry_learn_more->setOpenExternalLinks(true); | ||
| 33 | ui->telemetry_learn_more->setText(tr("<a " | ||
| 34 | "href='https://citra-emu.org/entry/" | ||
| 35 | "telemetry-and-why-thats-a-good-thing/'><span " | ||
| 36 | "style=\"text-decoration: underline; " | ||
| 37 | "color:#039be5;\">Learn more</span></a>")); | ||
| 38 | |||
| 39 | ui->web_signup_link->setOpenExternalLinks(true); | ||
| 40 | ui->web_signup_link->setText( | ||
| 41 | tr("<a href='https://services.citra-emu.org/'><span style=\"text-decoration: underline; " | ||
| 42 | "color:#039be5;\">Sign up</span></a>")); | ||
| 43 | ui->web_token_info_link->setOpenExternalLinks(true); | ||
| 44 | ui->web_token_info_link->setText( | ||
| 45 | tr("<a href='https://citra-emu.org/wiki/citra-web-service/'><span style=\"text-decoration: " | ||
| 46 | "underline; color:#039be5;\">What is my token?</span></a>")); | ||
| 47 | |||
| 48 | ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); | ||
| 49 | ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username)); | ||
| 50 | ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token)); | ||
| 51 | // Connect after setting the values, to avoid calling OnLoginChanged now | ||
| 52 | connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); | ||
| 53 | connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); | ||
| 54 | ui->label_telemetry_id->setText( | ||
| 55 | tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); | ||
| 56 | user_verified = true; | ||
| 57 | |||
| 58 | ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence); | ||
| 59 | } | ||
| 60 | |||
| 61 | void ConfigureWeb::applyConfiguration() { | ||
| 62 | Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); | ||
| 63 | UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); | ||
| 64 | if (user_verified) { | ||
| 65 | Settings::values.yuzu_username = ui->edit_username->text().toStdString(); | ||
| 66 | Settings::values.yuzu_token = ui->edit_token->text().toStdString(); | ||
| 67 | } else { | ||
| 68 | QMessageBox::warning(this, tr("Username and token not verified"), | ||
| 69 | tr("Username and token were not verified. The changes to your " | ||
| 70 | "username and/or token have not been saved.")); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | void ConfigureWeb::RefreshTelemetryID() { | ||
| 75 | const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; | ||
| 76 | ui->label_telemetry_id->setText( | ||
| 77 | tr("Telemetry ID: 0x%1").arg(QString::number(new_telemetry_id, 16).toUpper())); | ||
| 78 | } | ||
| 79 | |||
| 80 | void ConfigureWeb::OnLoginChanged() { | ||
| 81 | if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { | ||
| 82 | user_verified = true; | ||
| 83 | ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||
| 84 | ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||
| 85 | } else { | ||
| 86 | user_verified = false; | ||
| 87 | ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||
| 88 | ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | void ConfigureWeb::VerifyLogin() { | ||
| 93 | ui->button_verify_login->setDisabled(true); | ||
| 94 | ui->button_verify_login->setText(tr("Verifying")); | ||
| 95 | verify_watcher.setFuture( | ||
| 96 | QtConcurrent::run([this, username = ui->edit_username->text().toStdString(), | ||
| 97 | token = ui->edit_token->text().toStdString()]() { | ||
| 98 | return Core::VerifyLogin(username, token); | ||
| 99 | })); | ||
| 100 | } | ||
| 101 | |||
| 102 | void ConfigureWeb::OnLoginVerified() { | ||
| 103 | ui->button_verify_login->setEnabled(true); | ||
| 104 | ui->button_verify_login->setText(tr("Verify")); | ||
| 105 | if (verify_watcher.result()) { | ||
| 106 | user_verified = true; | ||
| 107 | ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||
| 108 | ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||
| 109 | } else { | ||
| 110 | ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||
| 111 | ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||
| 112 | QMessageBox::critical( | ||
| 113 | this, tr("Verification failed"), | ||
| 114 | tr("Verification failed. Check that you have entered your username and token " | ||
| 115 | "correctly, and that your internet connection is working.")); | ||
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | void ConfigureWeb::retranslateUi() { | ||
| 120 | ui->retranslateUi(this); | ||
| 121 | } | ||
diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h new file mode 100644 index 000000000..7741ab95d --- /dev/null +++ b/src/yuzu/configuration/configure_web.h | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | // Copyright 2017 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include <memory> | ||
| 8 | #include <QFutureWatcher> | ||
| 9 | #include <QWidget> | ||
| 10 | |||
| 11 | namespace Ui { | ||
| 12 | class ConfigureWeb; | ||
| 13 | } | ||
| 14 | |||
| 15 | class ConfigureWeb : public QWidget { | ||
| 16 | Q_OBJECT | ||
| 17 | |||
| 18 | public: | ||
| 19 | explicit ConfigureWeb(QWidget* parent = nullptr); | ||
| 20 | ~ConfigureWeb(); | ||
| 21 | |||
| 22 | void applyConfiguration(); | ||
| 23 | void retranslateUi(); | ||
| 24 | |||
| 25 | public slots: | ||
| 26 | void RefreshTelemetryID(); | ||
| 27 | void OnLoginChanged(); | ||
| 28 | void VerifyLogin(); | ||
| 29 | void OnLoginVerified(); | ||
| 30 | |||
| 31 | private: | ||
| 32 | void setConfiguration(); | ||
| 33 | |||
| 34 | bool user_verified = true; | ||
| 35 | QFutureWatcher<bool> verify_watcher; | ||
| 36 | |||
| 37 | std::unique_ptr<Ui::ConfigureWeb> ui; | ||
| 38 | }; | ||
diff --git a/src/yuzu/configuration/configure_web.ui b/src/yuzu/configuration/configure_web.ui new file mode 100644 index 000000000..2f4b9dd73 --- /dev/null +++ b/src/yuzu/configuration/configure_web.ui | |||
| @@ -0,0 +1,206 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <ui version="4.0"> | ||
| 3 | <class>ConfigureWeb</class> | ||
| 4 | <widget class="QWidget" name="ConfigureWeb"> | ||
| 5 | <property name="geometry"> | ||
| 6 | <rect> | ||
| 7 | <x>0</x> | ||
| 8 | <y>0</y> | ||
| 9 | <width>926</width> | ||
| 10 | <height>561</height> | ||
| 11 | </rect> | ||
| 12 | </property> | ||
| 13 | <property name="windowTitle"> | ||
| 14 | <string>Form</string> | ||
| 15 | </property> | ||
| 16 | <layout class="QVBoxLayout" name="verticalLayout"> | ||
| 17 | <item> | ||
| 18 | <layout class="QVBoxLayout" name="verticalLayout_3"> | ||
| 19 | <item> | ||
| 20 | <widget class="QGroupBox" name="groupBoxWebConfig"> | ||
| 21 | <property name="title"> | ||
| 22 | <string>yuzu Web Service</string> | ||
| 23 | </property> | ||
| 24 | <layout class="QVBoxLayout" name="verticalLayoutYuzuWebService"> | ||
| 25 | <item> | ||
| 26 | <widget class="QLabel" name="web_credentials_disclaimer"> | ||
| 27 | <property name="text"> | ||
| 28 | <string>By providing your username and token, you agree to allow yuzu to collect additional usage data, which may include user identifying information.</string> | ||
| 29 | </property> | ||
| 30 | </widget> | ||
| 31 | </item> | ||
| 32 | <item> | ||
| 33 | <layout class="QGridLayout" name="gridLayoutYuzuUsername"> | ||
| 34 | <item row="2" column="3"> | ||
| 35 | <widget class="QPushButton" name="button_verify_login"> | ||
| 36 | <property name="sizePolicy"> | ||
| 37 | <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> | ||
| 38 | <horstretch>0</horstretch> | ||
| 39 | <verstretch>0</verstretch> | ||
| 40 | </sizepolicy> | ||
| 41 | </property> | ||
| 42 | <property name="layoutDirection"> | ||
| 43 | <enum>Qt::RightToLeft</enum> | ||
| 44 | </property> | ||
| 45 | <property name="text"> | ||
| 46 | <string>Verify</string> | ||
| 47 | </property> | ||
| 48 | </widget> | ||
| 49 | </item> | ||
| 50 | <item row="2" column="0"> | ||
| 51 | <widget class="QLabel" name="web_signup_link"> | ||
| 52 | <property name="text"> | ||
| 53 | <string>Sign up</string> | ||
| 54 | </property> | ||
| 55 | </widget> | ||
| 56 | </item> | ||
| 57 | <item row="0" column="1" colspan="3"> | ||
| 58 | <widget class="QLineEdit" name="edit_username"> | ||
| 59 | <property name="maxLength"> | ||
| 60 | <number>36</number> | ||
| 61 | </property> | ||
| 62 | </widget> | ||
| 63 | </item> | ||
| 64 | <item row="1" column="0"> | ||
| 65 | <widget class="QLabel" name="label_token"> | ||
| 66 | <property name="text"> | ||
| 67 | <string>Token: </string> | ||
| 68 | </property> | ||
| 69 | </widget> | ||
| 70 | </item> | ||
| 71 | <item row="1" column="4"> | ||
| 72 | <widget class="QLabel" name="label_token_verified"> | ||
| 73 | </widget> | ||
| 74 | </item> | ||
| 75 | <item row="0" column="0"> | ||
| 76 | <widget class="QLabel" name="label_username"> | ||
| 77 | <property name="text"> | ||
| 78 | <string>Username: </string> | ||
| 79 | </property> | ||
| 80 | </widget> | ||
| 81 | </item> | ||
| 82 | <item row="0" column="4"> | ||
| 83 | <widget class="QLabel" name="label_username_verified"> | ||
| 84 | </widget> | ||
| 85 | </item> | ||
| 86 | <item row="1" column="1" colspan="3"> | ||
| 87 | <widget class="QLineEdit" name="edit_token"> | ||
| 88 | <property name="maxLength"> | ||
| 89 | <number>36</number> | ||
| 90 | </property> | ||
| 91 | <property name="echoMode"> | ||
| 92 | <enum>QLineEdit::Password</enum> | ||
| 93 | </property> | ||
| 94 | </widget> | ||
| 95 | </item> | ||
| 96 | <item row="2" column="1"> | ||
| 97 | <widget class="QLabel" name="web_token_info_link"> | ||
| 98 | <property name="text"> | ||
| 99 | <string>What is my token?</string> | ||
| 100 | </property> | ||
| 101 | </widget> | ||
| 102 | </item> | ||
| 103 | <item row="2" column="2"> | ||
| 104 | <spacer name="horizontalSpacer"> | ||
| 105 | <property name="orientation"> | ||
| 106 | <enum>Qt::Horizontal</enum> | ||
| 107 | </property> | ||
| 108 | <property name="sizeHint" stdset="0"> | ||
| 109 | <size> | ||
| 110 | <width>40</width> | ||
| 111 | <height>20</height> | ||
| 112 | </size> | ||
| 113 | </property> | ||
| 114 | </spacer> | ||
| 115 | </item> | ||
| 116 | </layout> | ||
| 117 | </item> | ||
| 118 | </layout> | ||
| 119 | </widget> | ||
| 120 | </item> | ||
| 121 | <item> | ||
| 122 | <widget class="QGroupBox" name="groupBox"> | ||
| 123 | <property name="title"> | ||
| 124 | <string>Telemetry</string> | ||
| 125 | </property> | ||
| 126 | <layout class="QVBoxLayout" name="verticalLayout_2"> | ||
| 127 | <item> | ||
| 128 | <widget class="QCheckBox" name="toggle_telemetry"> | ||
| 129 | <property name="text"> | ||
| 130 | <string>Share anonymous usage data with the yuzu team</string> | ||
| 131 | </property> | ||
| 132 | </widget> | ||
| 133 | </item> | ||
| 134 | <item> | ||
| 135 | <widget class="QLabel" name="telemetry_learn_more"> | ||
| 136 | <property name="text"> | ||
| 137 | <string>Learn more</string> | ||
| 138 | </property> | ||
| 139 | </widget> | ||
| 140 | </item> | ||
| 141 | <item> | ||
| 142 | <layout class="QGridLayout" name="gridLayoutTelemetryId"> | ||
| 143 | <item row="0" column="0"> | ||
| 144 | <widget class="QLabel" name="label_telemetry_id"> | ||
| 145 | <property name="text"> | ||
| 146 | <string>Telemetry ID:</string> | ||
| 147 | </property> | ||
| 148 | </widget> | ||
| 149 | </item> | ||
| 150 | <item row="0" column="1"> | ||
| 151 | <widget class="QPushButton" name="button_regenerate_telemetry_id"> | ||
| 152 | <property name="sizePolicy"> | ||
| 153 | <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> | ||
| 154 | <horstretch>0</horstretch> | ||
| 155 | <verstretch>0</verstretch> | ||
| 156 | </sizepolicy> | ||
| 157 | </property> | ||
| 158 | <property name="layoutDirection"> | ||
| 159 | <enum>Qt::RightToLeft</enum> | ||
| 160 | </property> | ||
| 161 | <property name="text"> | ||
| 162 | <string>Regenerate</string> | ||
| 163 | </property> | ||
| 164 | </widget> | ||
| 165 | </item> | ||
| 166 | </layout> | ||
| 167 | </item> | ||
| 168 | </layout> | ||
| 169 | </widget> | ||
| 170 | </item> | ||
| 171 | </layout> | ||
| 172 | </item> | ||
| 173 | <item> | ||
| 174 | <widget class="QGroupBox" name="discord_group"> | ||
| 175 | <property name="title"> | ||
| 176 | <string>Discord Presence</string> | ||
| 177 | </property> | ||
| 178 | <layout class="QVBoxLayout" name="verticalLayout_21"> | ||
| 179 | <item> | ||
| 180 | <widget class="QCheckBox" name="toggle_discordrpc"> | ||
| 181 | <property name="text"> | ||
| 182 | <string>Show Current Game in your Discord Status</string> | ||
| 183 | </property> | ||
| 184 | </widget> | ||
| 185 | </item> | ||
| 186 | </layout> | ||
| 187 | </widget> | ||
| 188 | </item> | ||
| 189 | <item> | ||
| 190 | <spacer name="verticalSpacer"> | ||
| 191 | <property name="orientation"> | ||
| 192 | <enum>Qt::Vertical</enum> | ||
| 193 | </property> | ||
| 194 | <property name="sizeHint" stdset="0"> | ||
| 195 | <size> | ||
| 196 | <width>20</width> | ||
| 197 | <height>40</height> | ||
| 198 | </size> | ||
| 199 | </property> | ||
| 200 | </spacer> | ||
| 201 | </item> | ||
| 202 | </layout> | ||
| 203 | </widget> | ||
| 204 | <resources/> | ||
| 205 | <connections/> | ||
| 206 | </ui> | ||
diff --git a/src/yuzu/discord.h b/src/yuzu/discord.h new file mode 100644 index 000000000..a867cc4d6 --- /dev/null +++ b/src/yuzu/discord.h | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | namespace DiscordRPC { | ||
| 8 | |||
| 9 | class DiscordInterface { | ||
| 10 | public: | ||
| 11 | virtual ~DiscordInterface() = default; | ||
| 12 | |||
| 13 | virtual void Pause() = 0; | ||
| 14 | virtual void Update() = 0; | ||
| 15 | }; | ||
| 16 | |||
| 17 | class NullImpl : public DiscordInterface { | ||
| 18 | public: | ||
| 19 | ~NullImpl() = default; | ||
| 20 | |||
| 21 | void Pause() override {} | ||
| 22 | void Update() override {} | ||
| 23 | }; | ||
| 24 | |||
| 25 | } // namespace DiscordRPC | ||
diff --git a/src/yuzu/discord_impl.cpp b/src/yuzu/discord_impl.cpp new file mode 100644 index 000000000..9d87a41eb --- /dev/null +++ b/src/yuzu/discord_impl.cpp | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #include <chrono> | ||
| 6 | #include <string> | ||
| 7 | #include <discord_rpc.h> | ||
| 8 | #include "common/common_types.h" | ||
| 9 | #include "core/core.h" | ||
| 10 | #include "core/loader/loader.h" | ||
| 11 | #include "yuzu/discord_impl.h" | ||
| 12 | #include "yuzu/ui_settings.h" | ||
| 13 | |||
| 14 | namespace DiscordRPC { | ||
| 15 | |||
| 16 | DiscordImpl::DiscordImpl() { | ||
| 17 | DiscordEventHandlers handlers{}; | ||
| 18 | |||
| 19 | // The number is the client ID for yuzu, it's used for images and the | ||
| 20 | // application name | ||
| 21 | Discord_Initialize("471872241299226636", &handlers, 1, nullptr); | ||
| 22 | } | ||
| 23 | |||
| 24 | DiscordImpl::~DiscordImpl() { | ||
| 25 | Discord_ClearPresence(); | ||
| 26 | Discord_Shutdown(); | ||
| 27 | } | ||
| 28 | |||
| 29 | void DiscordImpl::Pause() { | ||
| 30 | Discord_ClearPresence(); | ||
| 31 | } | ||
| 32 | |||
| 33 | void DiscordImpl::Update() { | ||
| 34 | s64 start_time = std::chrono::duration_cast<std::chrono::seconds>( | ||
| 35 | std::chrono::system_clock::now().time_since_epoch()) | ||
| 36 | .count(); | ||
| 37 | std::string title; | ||
| 38 | if (Core::System::GetInstance().IsPoweredOn()) | ||
| 39 | Core::System::GetInstance().GetAppLoader().ReadTitle(title); | ||
| 40 | DiscordRichPresence presence{}; | ||
| 41 | presence.largeImageKey = "yuzu_logo"; | ||
| 42 | presence.largeImageText = "yuzu is an emulator for the Nintendo Switch"; | ||
| 43 | if (Core::System::GetInstance().IsPoweredOn()) { | ||
| 44 | presence.state = title.c_str(); | ||
| 45 | presence.details = "Currently in game"; | ||
| 46 | } else { | ||
| 47 | presence.details = "Not in game"; | ||
| 48 | } | ||
| 49 | presence.startTimestamp = start_time; | ||
| 50 | Discord_UpdatePresence(&presence); | ||
| 51 | } | ||
| 52 | } // namespace DiscordRPC | ||
diff --git a/src/yuzu/discord_impl.h b/src/yuzu/discord_impl.h new file mode 100644 index 000000000..d71428c10 --- /dev/null +++ b/src/yuzu/discord_impl.h | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | // Copyright 2018 Citra Emulator Project | ||
| 2 | // Licensed under GPLv2 or any later version | ||
| 3 | // Refer to the license.txt file included. | ||
| 4 | |||
| 5 | #pragma once | ||
| 6 | |||
| 7 | #include "yuzu/discord.h" | ||
| 8 | |||
| 9 | namespace DiscordRPC { | ||
| 10 | |||
| 11 | class DiscordImpl : public DiscordInterface { | ||
| 12 | public: | ||
| 13 | DiscordImpl(); | ||
| 14 | ~DiscordImpl(); | ||
| 15 | |||
| 16 | void Pause() override; | ||
| 17 | void Update() override; | ||
| 18 | }; | ||
| 19 | |||
| 20 | } // namespace DiscordRPC | ||
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 27015d02c..2d6e0d4fc 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -35,6 +35,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | |||
| 35 | #include <QtWidgets> | 35 | #include <QtWidgets> |
| 36 | #include <fmt/format.h> | 36 | #include <fmt/format.h> |
| 37 | #include "common/common_paths.h" | 37 | #include "common/common_paths.h" |
| 38 | #include "common/detached_tasks.h" | ||
| 38 | #include "common/file_util.h" | 39 | #include "common/file_util.h" |
| 39 | #include "common/logging/backend.h" | 40 | #include "common/logging/backend.h" |
| 40 | #include "common/logging/filter.h" | 41 | #include "common/logging/filter.h" |
| @@ -65,6 +66,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | |||
| 65 | #include "video_core/debug_utils/debug_utils.h" | 66 | #include "video_core/debug_utils/debug_utils.h" |
| 66 | #include "yuzu/about_dialog.h" | 67 | #include "yuzu/about_dialog.h" |
| 67 | #include "yuzu/bootmanager.h" | 68 | #include "yuzu/bootmanager.h" |
| 69 | #include "yuzu/compatdb.h" | ||
| 68 | #include "yuzu/compatibility_list.h" | 70 | #include "yuzu/compatibility_list.h" |
| 69 | #include "yuzu/configuration/config.h" | 71 | #include "yuzu/configuration/config.h" |
| 70 | #include "yuzu/configuration/configure_dialog.h" | 72 | #include "yuzu/configuration/configure_dialog.h" |
| @@ -73,12 +75,17 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | |||
| 73 | #include "yuzu/debugger/graphics/graphics_surface.h" | 75 | #include "yuzu/debugger/graphics/graphics_surface.h" |
| 74 | #include "yuzu/debugger/profiler.h" | 76 | #include "yuzu/debugger/profiler.h" |
| 75 | #include "yuzu/debugger/wait_tree.h" | 77 | #include "yuzu/debugger/wait_tree.h" |
| 78 | #include "yuzu/discord.h" | ||
| 76 | #include "yuzu/game_list.h" | 79 | #include "yuzu/game_list.h" |
| 77 | #include "yuzu/game_list_p.h" | 80 | #include "yuzu/game_list_p.h" |
| 78 | #include "yuzu/hotkeys.h" | 81 | #include "yuzu/hotkeys.h" |
| 79 | #include "yuzu/main.h" | 82 | #include "yuzu/main.h" |
| 80 | #include "yuzu/ui_settings.h" | 83 | #include "yuzu/ui_settings.h" |
| 81 | 84 | ||
| 85 | #ifdef USE_DISCORD_PRESENCE | ||
| 86 | #include "yuzu/discord_impl.h" | ||
| 87 | #endif | ||
| 88 | |||
| 82 | #ifdef QT_STATICPLUGIN | 89 | #ifdef QT_STATICPLUGIN |
| 83 | Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); | 90 | Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); |
| 84 | #endif | 91 | #endif |
| @@ -102,23 +109,22 @@ enum class CalloutFlag : uint32_t { | |||
| 102 | DRDDeprecation = 0x2, | 109 | DRDDeprecation = 0x2, |
| 103 | }; | 110 | }; |
| 104 | 111 | ||
| 105 | static void ShowCalloutMessage(const QString& message, CalloutFlag flag) { | 112 | void GMainWindow::ShowTelemetryCallout() { |
| 106 | if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) { | 113 | if (UISettings::values.callout_flags & static_cast<uint32_t>(CalloutFlag::Telemetry)) { |
| 107 | return; | 114 | return; |
| 108 | } | 115 | } |
| 109 | 116 | ||
| 110 | UISettings::values.callout_flags |= static_cast<uint32_t>(flag); | 117 | UISettings::values.callout_flags |= static_cast<uint32_t>(CalloutFlag::Telemetry); |
| 111 | 118 | static const QString telemetry_message = | |
| 112 | QMessageBox msg; | 119 | tr("<a href='https://citra-emu.org/entry/telemetry-and-why-thats-a-good-thing/'>Anonymous " |
| 113 | msg.setText(message); | 120 | "data is collected</a> to help improve yuzu. " |
| 114 | msg.setStandardButtons(QMessageBox::Ok); | 121 | "<br/><br/>Would you like to share your usage data with us?"); |
| 115 | msg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); | 122 | if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) { |
| 116 | msg.setStyleSheet("QLabel{min-width: 900px;}"); | 123 | Settings::values.enable_telemetry = false; |
| 117 | msg.exec(); | 124 | Settings::Apply(); |
| 125 | } | ||
| 118 | } | 126 | } |
| 119 | 127 | ||
| 120 | void GMainWindow::ShowCallouts() {} | ||
| 121 | |||
| 122 | const int GMainWindow::max_recent_files_item; | 128 | const int GMainWindow::max_recent_files_item; |
| 123 | 129 | ||
| 124 | static void InitializeLogging() { | 130 | static void InitializeLogging() { |
| @@ -145,6 +151,9 @@ GMainWindow::GMainWindow() | |||
| 145 | default_theme_paths = QIcon::themeSearchPaths(); | 151 | default_theme_paths = QIcon::themeSearchPaths(); |
| 146 | UpdateUITheme(); | 152 | UpdateUITheme(); |
| 147 | 153 | ||
| 154 | SetDiscordEnabled(UISettings::values.enable_discord_presence); | ||
| 155 | discord_rpc->Update(); | ||
| 156 | |||
| 148 | InitializeWidgets(); | 157 | InitializeWidgets(); |
| 149 | InitializeDebugWidgets(); | 158 | InitializeDebugWidgets(); |
| 150 | InitializeRecentFileMenuActions(); | 159 | InitializeRecentFileMenuActions(); |
| @@ -168,7 +177,7 @@ GMainWindow::GMainWindow() | |||
| 168 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | 177 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); |
| 169 | 178 | ||
| 170 | // Show one-time "callout" messages to the user | 179 | // Show one-time "callout" messages to the user |
| 171 | ShowCallouts(); | 180 | ShowTelemetryCallout(); |
| 172 | 181 | ||
| 173 | QStringList args = QApplication::arguments(); | 182 | QStringList args = QApplication::arguments(); |
| 174 | if (args.length() >= 2) { | 183 | if (args.length() >= 2) { |
| @@ -183,6 +192,9 @@ GMainWindow::~GMainWindow() { | |||
| 183 | } | 192 | } |
| 184 | 193 | ||
| 185 | void GMainWindow::InitializeWidgets() { | 194 | void GMainWindow::InitializeWidgets() { |
| 195 | #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING | ||
| 196 | ui.action_Report_Compatibility->setVisible(true); | ||
| 197 | #endif | ||
| 186 | render_window = new GRenderWindow(this, emu_thread.get()); | 198 | render_window = new GRenderWindow(this, emu_thread.get()); |
| 187 | render_window->hide(); | 199 | render_window->hide(); |
| 188 | 200 | ||
| @@ -411,6 +423,8 @@ void GMainWindow::ConnectMenuEvents() { | |||
| 411 | connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame); | 423 | connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame); |
| 412 | connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame); | 424 | connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame); |
| 413 | connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame); | 425 | connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame); |
| 426 | connect(ui.action_Report_Compatibility, &QAction::triggered, this, | ||
| 427 | &GMainWindow::OnMenuReportCompatibility); | ||
| 414 | connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); | 428 | connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); |
| 415 | connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); | 429 | connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); |
| 416 | 430 | ||
| @@ -647,6 +661,7 @@ void GMainWindow::BootGame(const QString& filename) { | |||
| 647 | } | 661 | } |
| 648 | 662 | ||
| 649 | void GMainWindow::ShutdownGame() { | 663 | void GMainWindow::ShutdownGame() { |
| 664 | discord_rpc->Pause(); | ||
| 650 | emu_thread->RequestStop(); | 665 | emu_thread->RequestStop(); |
| 651 | 666 | ||
| 652 | emit EmulationStopping(); | 667 | emit EmulationStopping(); |
| @@ -655,6 +670,8 @@ void GMainWindow::ShutdownGame() { | |||
| 655 | emu_thread->wait(); | 670 | emu_thread->wait(); |
| 656 | emu_thread = nullptr; | 671 | emu_thread = nullptr; |
| 657 | 672 | ||
| 673 | discord_rpc->Update(); | ||
| 674 | |||
| 658 | // The emulation is stopped, so closing the window or not does not matter anymore | 675 | // The emulation is stopped, so closing the window or not does not matter anymore |
| 659 | disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); | 676 | disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); |
| 660 | 677 | ||
| @@ -664,6 +681,7 @@ void GMainWindow::ShutdownGame() { | |||
| 664 | ui.action_Pause->setEnabled(false); | 681 | ui.action_Pause->setEnabled(false); |
| 665 | ui.action_Stop->setEnabled(false); | 682 | ui.action_Stop->setEnabled(false); |
| 666 | ui.action_Restart->setEnabled(false); | 683 | ui.action_Restart->setEnabled(false); |
| 684 | ui.action_Report_Compatibility->setEnabled(false); | ||
| 667 | render_window->hide(); | 685 | render_window->hide(); |
| 668 | game_list->show(); | 686 | game_list->show(); |
| 669 | game_list->setFilterFocus(); | 687 | game_list->setFilterFocus(); |
| @@ -1147,6 +1165,9 @@ void GMainWindow::OnStartGame() { | |||
| 1147 | ui.action_Pause->setEnabled(true); | 1165 | ui.action_Pause->setEnabled(true); |
| 1148 | ui.action_Stop->setEnabled(true); | 1166 | ui.action_Stop->setEnabled(true); |
| 1149 | ui.action_Restart->setEnabled(true); | 1167 | ui.action_Restart->setEnabled(true); |
| 1168 | ui.action_Report_Compatibility->setEnabled(true); | ||
| 1169 | |||
| 1170 | discord_rpc->Update(); | ||
| 1150 | } | 1171 | } |
| 1151 | 1172 | ||
| 1152 | void GMainWindow::OnPauseGame() { | 1173 | void GMainWindow::OnPauseGame() { |
| @@ -1161,6 +1182,20 @@ void GMainWindow::OnStopGame() { | |||
| 1161 | ShutdownGame(); | 1182 | ShutdownGame(); |
| 1162 | } | 1183 | } |
| 1163 | 1184 | ||
| 1185 | void GMainWindow::OnMenuReportCompatibility() { | ||
| 1186 | if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) { | ||
| 1187 | CompatDB compatdb{this}; | ||
| 1188 | compatdb.exec(); | ||
| 1189 | } else { | ||
| 1190 | QMessageBox::critical( | ||
| 1191 | this, tr("Missing yuzu Account"), | ||
| 1192 | tr("In order to submit a game compatibility test case, you must link your yuzu " | ||
| 1193 | "account.<br><br/>To link your yuzu account, go to Emulation > Configuration " | ||
| 1194 | "> " | ||
| 1195 | "Web.")); | ||
| 1196 | } | ||
| 1197 | } | ||
| 1198 | |||
| 1164 | void GMainWindow::ToggleFullscreen() { | 1199 | void GMainWindow::ToggleFullscreen() { |
| 1165 | if (!emulation_running) { | 1200 | if (!emulation_running) { |
| 1166 | return; | 1201 | return; |
| @@ -1224,11 +1259,14 @@ void GMainWindow::ToggleWindowMode() { | |||
| 1224 | void GMainWindow::OnConfigure() { | 1259 | void GMainWindow::OnConfigure() { |
| 1225 | ConfigureDialog configureDialog(this, hotkey_registry); | 1260 | ConfigureDialog configureDialog(this, hotkey_registry); |
| 1226 | auto old_theme = UISettings::values.theme; | 1261 | auto old_theme = UISettings::values.theme; |
| 1262 | const bool old_discord_presence = UISettings::values.enable_discord_presence; | ||
| 1227 | auto result = configureDialog.exec(); | 1263 | auto result = configureDialog.exec(); |
| 1228 | if (result == QDialog::Accepted) { | 1264 | if (result == QDialog::Accepted) { |
| 1229 | configureDialog.applyConfiguration(); | 1265 | configureDialog.applyConfiguration(); |
| 1230 | if (UISettings::values.theme != old_theme) | 1266 | if (UISettings::values.theme != old_theme) |
| 1231 | UpdateUITheme(); | 1267 | UpdateUITheme(); |
| 1268 | if (UISettings::values.enable_discord_presence != old_discord_presence) | ||
| 1269 | SetDiscordEnabled(UISettings::values.enable_discord_presence); | ||
| 1232 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | 1270 | game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); |
| 1233 | config->Save(); | 1271 | config->Save(); |
| 1234 | } | 1272 | } |
| @@ -1443,11 +1481,25 @@ void GMainWindow::UpdateUITheme() { | |||
| 1443 | emit UpdateThemedIcons(); | 1481 | emit UpdateThemedIcons(); |
| 1444 | } | 1482 | } |
| 1445 | 1483 | ||
| 1484 | void GMainWindow::SetDiscordEnabled(bool state) { | ||
| 1485 | #ifdef USE_DISCORD_PRESENCE | ||
| 1486 | if (state) { | ||
| 1487 | discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>(); | ||
| 1488 | } else { | ||
| 1489 | discord_rpc = std::make_unique<DiscordRPC::NullImpl>(); | ||
| 1490 | } | ||
| 1491 | #else | ||
| 1492 | discord_rpc = std::make_unique<DiscordRPC::NullImpl>(); | ||
| 1493 | #endif | ||
| 1494 | discord_rpc->Update(); | ||
| 1495 | } | ||
| 1496 | |||
| 1446 | #ifdef main | 1497 | #ifdef main |
| 1447 | #undef main | 1498 | #undef main |
| 1448 | #endif | 1499 | #endif |
| 1449 | 1500 | ||
| 1450 | int main(int argc, char* argv[]) { | 1501 | int main(int argc, char* argv[]) { |
| 1502 | Common::DetachedTasks detached_tasks; | ||
| 1451 | MicroProfileOnThreadCreate("Frontend"); | 1503 | MicroProfileOnThreadCreate("Frontend"); |
| 1452 | SCOPE_EXIT({ MicroProfileShutdown(); }); | 1504 | SCOPE_EXIT({ MicroProfileShutdown(); }); |
| 1453 | 1505 | ||
| @@ -1465,5 +1517,7 @@ int main(int argc, char* argv[]) { | |||
| 1465 | GMainWindow main_window; | 1517 | GMainWindow main_window; |
| 1466 | // After settings have been loaded by GMainWindow, apply the filter | 1518 | // After settings have been loaded by GMainWindow, apply the filter |
| 1467 | main_window.show(); | 1519 | main_window.show(); |
| 1468 | return app.exec(); | 1520 | int result = app.exec(); |
| 1521 | detached_tasks.WaitForAllTasks(); | ||
| 1522 | return result; | ||
| 1469 | } | 1523 | } |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 8ee9242b1..fe0e9a50a 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -41,6 +41,10 @@ enum class EmulatedDirectoryTarget { | |||
| 41 | SDMC, | 41 | SDMC, |
| 42 | }; | 42 | }; |
| 43 | 43 | ||
| 44 | namespace DiscordRPC { | ||
| 45 | class DiscordInterface; | ||
| 46 | } | ||
| 47 | |||
| 44 | class GMainWindow : public QMainWindow { | 48 | class GMainWindow : public QMainWindow { |
| 45 | Q_OBJECT | 49 | Q_OBJECT |
| 46 | 50 | ||
| @@ -61,6 +65,8 @@ public: | |||
| 61 | GMainWindow(); | 65 | GMainWindow(); |
| 62 | ~GMainWindow() override; | 66 | ~GMainWindow() override; |
| 63 | 67 | ||
| 68 | std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; | ||
| 69 | |||
| 64 | signals: | 70 | signals: |
| 65 | 71 | ||
| 66 | /** | 72 | /** |
| @@ -99,7 +105,8 @@ private: | |||
| 99 | void BootGame(const QString& filename); | 105 | void BootGame(const QString& filename); |
| 100 | void ShutdownGame(); | 106 | void ShutdownGame(); |
| 101 | 107 | ||
| 102 | void ShowCallouts(); | 108 | void ShowTelemetryCallout(); |
| 109 | void SetDiscordEnabled(bool state); | ||
| 103 | 110 | ||
| 104 | /** | 111 | /** |
| 105 | * Stores the filename in the recently loaded files list. | 112 | * Stores the filename in the recently loaded files list. |
| @@ -135,6 +142,7 @@ private slots: | |||
| 135 | void OnStartGame(); | 142 | void OnStartGame(); |
| 136 | void OnPauseGame(); | 143 | void OnPauseGame(); |
| 137 | void OnStopGame(); | 144 | void OnStopGame(); |
| 145 | void OnMenuReportCompatibility(); | ||
| 138 | /// Called whenever a user selects a game in the game list widget. | 146 | /// Called whenever a user selects a game in the game list widget. |
| 139 | void OnGameListLoadFile(QString game_path); | 147 | void OnGameListLoadFile(QString game_path); |
| 140 | void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); | 148 | void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); |
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 3879d4813..cb1664b21 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui | |||
| @@ -45,7 +45,7 @@ | |||
| 45 | <x>0</x> | 45 | <x>0</x> |
| 46 | <y>0</y> | 46 | <y>0</y> |
| 47 | <width>1081</width> | 47 | <width>1081</width> |
| 48 | <height>19</height> | 48 | <height>21</height> |
| 49 | </rect> | 49 | </rect> |
| 50 | </property> | 50 | </property> |
| 51 | <widget class="QMenu" name="menu_File"> | 51 | <widget class="QMenu" name="menu_File"> |
| @@ -101,6 +101,8 @@ | |||
| 101 | <property name="title"> | 101 | <property name="title"> |
| 102 | <string>&Help</string> | 102 | <string>&Help</string> |
| 103 | </property> | 103 | </property> |
| 104 | <addaction name="action_Report_Compatibility"/> | ||
| 105 | <addaction name="separator"/> | ||
| 104 | <addaction name="action_About"/> | 106 | <addaction name="action_About"/> |
| 105 | </widget> | 107 | </widget> |
| 106 | <addaction name="menu_File"/> | 108 | <addaction name="menu_File"/> |
| @@ -239,6 +241,18 @@ | |||
| 239 | <string>Restart</string> | 241 | <string>Restart</string> |
| 240 | </property> | 242 | </property> |
| 241 | </action> | 243 | </action> |
| 244 | <action name="action_Report_Compatibility"> | ||
| 245 | <property name="enabled"> | ||
| 246 | <bool>false</bool> | ||
| 247 | </property> | ||
| 248 | <property name="text"> | ||
| 249 | <string>Report Compatibility</string> | ||
| 250 | </property> | ||
| 251 | <property name="visible"> | ||
| 252 | <bool>false</bool> | ||
| 253 | </property> | ||
| 254 | </action> | ||
| 242 | </widget> | 255 | </widget> |
| 243 | <resources/> | 256 | <resources/> |
| 257 | <connections/> | ||
| 244 | </ui> | 258 | </ui> |
diff --git a/src/yuzu/ui_settings.h b/src/yuzu/ui_settings.h index 051494bc5..89d9140f3 100644 --- a/src/yuzu/ui_settings.h +++ b/src/yuzu/ui_settings.h | |||
| @@ -39,6 +39,9 @@ struct Values { | |||
| 39 | bool confirm_before_closing; | 39 | bool confirm_before_closing; |
| 40 | bool first_start; | 40 | bool first_start; |
| 41 | 41 | ||
| 42 | // Discord RPC | ||
| 43 | bool enable_discord_presence; | ||
| 44 | |||
| 42 | QString roms_path; | 45 | QString roms_path; |
| 43 | QString symbols_path; | 46 | QString symbols_path; |
| 44 | QString gamedir; | 47 | QString gamedir; |
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index a478b0a56..9d934e220 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp | |||
| @@ -138,6 +138,14 @@ void Config::ReadValues() { | |||
| 138 | Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false); | 138 | Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false); |
| 139 | Settings::values.gdbstub_port = | 139 | Settings::values.gdbstub_port = |
| 140 | static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689)); | 140 | static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689)); |
| 141 | |||
| 142 | // Web Service | ||
| 143 | Settings::values.enable_telemetry = | ||
| 144 | sdl2_config->GetBoolean("WebService", "enable_telemetry", true); | ||
| 145 | Settings::values.web_api_url = | ||
| 146 | sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org"); | ||
| 147 | Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", ""); | ||
| 148 | Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", ""); | ||
| 141 | } | 149 | } |
| 142 | 150 | ||
| 143 | void Config::Reload() { | 151 | void Config::Reload() { |
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index d35c441e9..eaa64da39 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h | |||
| @@ -202,10 +202,8 @@ gdbstub_port=24689 | |||
| 202 | # Whether or not to enable telemetry | 202 | # Whether or not to enable telemetry |
| 203 | # 0: No, 1 (default): Yes | 203 | # 0: No, 1 (default): Yes |
| 204 | enable_telemetry = | 204 | enable_telemetry = |
| 205 | # Endpoint URL for submitting telemetry data | 205 | # URL for Web API |
| 206 | telemetry_endpoint_url = | 206 | web_api_url = https://api.yuzu-emu.org |
| 207 | # Endpoint URL to verify the username and token | ||
| 208 | verify_endpoint_url = | ||
| 209 | # Username and token for yuzu Web Service | 207 | # Username and token for yuzu Web Service |
| 210 | # See https://services.citra-emu.org/ for more info | 208 | # See https://services.citra-emu.org/ for more info |
| 211 | yuzu_username = | 209 | yuzu_username = |
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index b2559b717..1d951ca3f 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp | |||
| @@ -10,6 +10,7 @@ | |||
| 10 | #include <fmt/ostream.h> | 10 | #include <fmt/ostream.h> |
| 11 | 11 | ||
| 12 | #include "common/common_paths.h" | 12 | #include "common/common_paths.h" |
| 13 | #include "common/detached_tasks.h" | ||
| 13 | #include "common/file_util.h" | 14 | #include "common/file_util.h" |
| 14 | #include "common/logging/backend.h" | 15 | #include "common/logging/backend.h" |
| 15 | #include "common/logging/filter.h" | 16 | #include "common/logging/filter.h" |
| @@ -78,6 +79,7 @@ static void InitializeLogging() { | |||
| 78 | 79 | ||
| 79 | /// Application entry point | 80 | /// Application entry point |
| 80 | int main(int argc, char** argv) { | 81 | int main(int argc, char** argv) { |
| 82 | Common::DetachedTasks detached_tasks; | ||
| 81 | Config config; | 83 | Config config; |
| 82 | 84 | ||
| 83 | int option_index = 0; | 85 | int option_index = 0; |
| @@ -213,5 +215,6 @@ int main(int argc, char** argv) { | |||
| 213 | system.RunLoop(); | 215 | system.RunLoop(); |
| 214 | } | 216 | } |
| 215 | 217 | ||
| 218 | detached_tasks.WaitForAllTasks(); | ||
| 216 | return 0; | 219 | return 0; |
| 217 | } | 220 | } |