summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis/common/travis-ci.env4
-rwxr-xr-x.travis/linux/build.sh2
-rwxr-xr-x.travis/linux/docker.sh2
-rwxr-xr-x.travis/macos/build.sh2
-rw-r--r--CMakeLists.txt11
-rw-r--r--appveyor.yml5
m---------externals/discord-rpc0
m---------externals/libressl0
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/common/CMakeLists.txt3
-rw-r--r--src/common/detached_tasks.cpp41
-rw-r--r--src/common/detached_tasks.h39
-rw-r--r--src/common/web_result.h24
-rw-r--r--src/core/CMakeLists.txt3
-rw-r--r--src/core/settings.h6
-rw-r--r--src/core/telemetry_session.cpp51
-rw-r--r--src/core/telemetry_session.h5
-rw-r--r--src/web_service/CMakeLists.txt16
-rw-r--r--src/web_service/json.h18
-rw-r--r--src/web_service/telemetry_json.cpp94
-rw-r--r--src/web_service/telemetry_json.h59
-rw-r--r--src/web_service/verify_login.cpp27
-rw-r--r--src/web_service/verify_login.h22
-rw-r--r--src/web_service/web_backend.cpp147
-rw-r--r--src/web_service/web_backend.h91
-rw-r--r--src/yuzu/CMakeLists.txt16
-rw-r--r--src/yuzu/compatdb.cpp61
-rw-r--r--src/yuzu/compatdb.h27
-rw-r--r--src/yuzu/compatdb.ui215
-rw-r--r--src/yuzu/configuration/config.cpp18
-rw-r--r--src/yuzu/configuration/configure.ui11
-rw-r--r--src/yuzu/configuration/configure_dialog.cpp1
-rw-r--r--src/yuzu/configuration/configure_web.cpp121
-rw-r--r--src/yuzu/configuration/configure_web.h38
-rw-r--r--src/yuzu/configuration/configure_web.ui206
-rw-r--r--src/yuzu/discord.h25
-rw-r--r--src/yuzu/discord_impl.cpp52
-rw-r--r--src/yuzu/discord_impl.h20
-rw-r--r--src/yuzu/main.cpp82
-rw-r--r--src/yuzu/main.h10
-rw-r--r--src/yuzu/main.ui16
-rw-r--r--src/yuzu/ui_settings.h3
-rw-r--r--src/yuzu_cmd/config.cpp8
-rw-r--r--src/yuzu_cmd/default_ini.h6
-rw-r--r--src/yuzu_cmd/yuzu.cpp3
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
10TRAVIS_JOB_NUMBER 10TRAVIS_JOB_NUMBER
11TRAVIS_REPO_SLUG 11TRAVIS_REPO_SLUG
12TRAVIS_TAG 12TRAVIS_TAG
13
14# yuzu specific flags
15ENABLE_COMPATIBILITY_REPORTING
16USE_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
3mkdir -p "$HOME/.ccache" 3mkdir -p "$HOME/.ccache"
4docker 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 4docker 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
6cd /yuzu 6cd /yuzu
7 7
8mkdir build && cd build 8mkdir build && cd build
9cmake .. -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 9cmake .. -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
10ninja 10ninja
11 11
12ccache -s 12ccache -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
10mkdir build && cd build 10mkdir build && cd build
11cmake --version 11cmake --version
12cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON 12cmake .. -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
13make -j4 13make -j4
14 14
15ccache -s 15ccache -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
15option(ENABLE_QT "Enable the Qt frontend" ON) 15option(ENABLE_QT "Enable the Qt frontend" ON)
16CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF) 16CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF)
17 17
18option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
19
18option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) 20option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON)
19 21
20option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) 22option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
21 23
24option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
25
22if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) 26if(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})
322endif() 326endif()
323 327
328if (ENABLE_WEB_SERVICE)
329 add_definitions(-DENABLE_WEB_SERVICE)
330endif()
331if (YUZU_ENABLE_COMPATIBILITY_REPORTING)
332 add_definitions(-DYUZU_ENABLE_COMPATIBILITY_REPORTING)
333endif()
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()
13if (ENABLE_QT) 13if (ENABLE_QT)
14 add_subdirectory(yuzu) 14 add_subdirectory(yuzu)
15endif() 15endif()
16if (ENABLE_WEB_SERVICE)
17 add_subdirectory(web_service)
18endif()
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
41add_library(common STATIC 41add_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
92if(ARCHITECTURE_x86_64) 95if(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
9namespace Common {
10
11DetachedTasks* DetachedTasks::instance = nullptr;
12
13DetachedTasks::DetachedTasks() {
14 ASSERT(instance == nullptr);
15 instance = this;
16}
17
18void DetachedTasks::WaitForAllTasks() {
19 std::unique_lock<std::mutex> lock(mutex);
20 cv.wait(lock, [this]() { return count == 0; });
21}
22
23DetachedTasks::~DetachedTasks() {
24 std::unique_lock<std::mutex> lock(mutex);
25 ASSERT(count == 0);
26 instance = nullptr;
27}
28
29void 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
9namespace 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 */
23class DetachedTasks {
24public:
25 DetachedTasks();
26 ~DetachedTasks();
27 void WaitForAllTasks();
28
29 static void AddTask(std::function<void()> task);
30
31private:
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
9namespace Common {
10struct 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
395target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) 395target_link_libraries(core PUBLIC common PRIVATE audio_core video_core)
396target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives) 396target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives)
397if (ENABLE_WEB_SERVICE)
398 target_link_libraries(core PUBLIC json-headers web_service)
399endif()
397 400
398if (ARCHITECTURE_x86_64) 401if (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
160void Apply(); 166void 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
16namespace Core { 23namespace Core {
17 24
18static u64 GenerateTelemetryId() { 25static 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
62std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func) { 91bool 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
73TelemetrySession::TelemetrySession() { 99TelemetrySession::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
34private: 35private:
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 */
58std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func); 59bool 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 @@
1add_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
10create_target_directory_groups(web_service)
11
12get_directory_property(OPENSSL_LIBS
13 DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl
14 DEFINITION OPENSSL_LIBS)
15add_definitions(-DCPPHTTPLIB_OPENSSL_SUPPORT)
16target_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
11namespace WebService {
12
13template <class T>
14void TelemetryJson::Serialize(Telemetry::FieldType type, const std::string& name, T value) {
15 sections[static_cast<u8>(type)][name] = value;
16}
17
18void TelemetryJson::SerializeSection(Telemetry::FieldType type, const std::string& name) {
19 TopSection()[name] = sections[static_cast<unsigned>(type)];
20}
21
22void TelemetryJson::Visit(const Telemetry::Field<bool>& field) {
23 Serialize(field.GetType(), field.GetName(), field.GetValue());
24}
25
26void TelemetryJson::Visit(const Telemetry::Field<double>& field) {
27 Serialize(field.GetType(), field.GetName(), field.GetValue());
28}
29
30void TelemetryJson::Visit(const Telemetry::Field<float>& field) {
31 Serialize(field.GetType(), field.GetName(), field.GetValue());
32}
33
34void TelemetryJson::Visit(const Telemetry::Field<u8>& field) {
35 Serialize(field.GetType(), field.GetName(), field.GetValue());
36}
37
38void TelemetryJson::Visit(const Telemetry::Field<u16>& field) {
39 Serialize(field.GetType(), field.GetName(), field.GetValue());
40}
41
42void TelemetryJson::Visit(const Telemetry::Field<u32>& field) {
43 Serialize(field.GetType(), field.GetName(), field.GetValue());
44}
45
46void TelemetryJson::Visit(const Telemetry::Field<u64>& field) {
47 Serialize(field.GetType(), field.GetName(), field.GetValue());
48}
49
50void TelemetryJson::Visit(const Telemetry::Field<s8>& field) {
51 Serialize(field.GetType(), field.GetName(), field.GetValue());
52}
53
54void TelemetryJson::Visit(const Telemetry::Field<s16>& field) {
55 Serialize(field.GetType(), field.GetName(), field.GetValue());
56}
57
58void TelemetryJson::Visit(const Telemetry::Field<s32>& field) {
59 Serialize(field.GetType(), field.GetName(), field.GetValue());
60}
61
62void TelemetryJson::Visit(const Telemetry::Field<s64>& field) {
63 Serialize(field.GetType(), field.GetName(), field.GetValue());
64}
65
66void TelemetryJson::Visit(const Telemetry::Field<std::string>& field) {
67 Serialize(field.GetType(), field.GetName(), field.GetValue());
68}
69
70void TelemetryJson::Visit(const Telemetry::Field<const char*>& field) {
71 Serialize(field.GetType(), field.GetName(), std::string(field.GetValue()));
72}
73
74void TelemetryJson::Visit(const Telemetry::Field<std::chrono::microseconds>& field) {
75 Serialize(field.GetType(), field.GetName(), field.GetValue().count());
76}
77
78void 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
13namespace WebService {
14
15/**
16 * Implementation of VisitorInterface that serialized telemetry into JSON, and submits it to the
17 * yuzu web service
18 */
19class TelemetryJson : public Telemetry::VisitorInterface {
20public:
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
42private:
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
9namespace WebService {
10
11bool 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
11namespace 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 */
20bool 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
14namespace WebService {
15
16static constexpr char API_VERSION[]{"1"};
17
18constexpr int HTTP_PORT = 80;
19constexpr int HTTPS_PORT = 443;
20
21constexpr int TIMEOUT_SECONDS = 30;
22
23Client::JWTCache Client::jwt_cache{};
24
25Client::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
32Common::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
113void 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
126Common::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
15namespace httplib {
16class Client;
17}
18
19namespace WebService {
20
21class Client {
22public:
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
59private:
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
77file(GLOB COMPAT_LIST 84file(GLOB COMPAT_LIST
@@ -113,6 +120,15 @@ target_link_libraries(yuzu PRIVATE common core input_common video_core)
113target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets) 120target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets)
114target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) 121target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
115 122
123if (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)
130endif()
131
116if(UNIX AND NOT APPLE) 132if(UNIX AND NOT APPLE)
117 install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") 133 install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
118endif() 134endif()
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
15CompatDB::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
28CompatDB::~CompatDB() = default;
29
30enum class CompatDBPage { Intro = 0, Selection = 1, Final = 2 };
31
32void 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
59void 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
10namespace Ui {
11class CompatDB;
12}
13
14class CompatDB : public QWizard {
15 Q_OBJECT
16
17public:
18 explicit CompatDB(QWidget* parent = nullptr);
19 ~CompatDB();
20
21private:
22 std::unique_ptr<Ui::CompatDB> ui;
23
24private 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;Should you choose to submit a test case to the &lt;/span&gt;&lt;a href=&quot;https://yuzu-emu.org/game/&quot;&gt;&lt;span style=&quot; font-size:10pt; text-decoration: underline; color:#0000ff;&quot;&gt;yuzu Compatibility List&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;, The following information will be collected and displayed on the site:&lt;/span&gt;&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Hardware Information (CPU / GPU / Operating System)&lt;/li&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Which version of yuzu you are running&lt;/li&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;The connected yuzu account&lt;/li&gt;&lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions flawlessly with no audio or graphical glitches.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The game crashes when attempting to startup.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Independent of speed or performance, how well does this game play from start to finish on this version of yuzu?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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
14ConfigureWeb::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
28ConfigureWeb::~ConfigureWeb() {}
29
30void 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
61void 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
74void 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
80void 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
92void 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
102void 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
119void 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
11namespace Ui {
12class ConfigureWeb;
13}
14
15class ConfigureWeb : public QWidget {
16 Q_OBJECT
17
18public:
19 explicit ConfigureWeb(QWidget* parent = nullptr);
20 ~ConfigureWeb();
21
22 void applyConfiguration();
23 void retranslateUi();
24
25public slots:
26 void RefreshTelemetryID();
27 void OnLoginChanged();
28 void VerifyLogin();
29 void OnLoginVerified();
30
31private:
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
7namespace DiscordRPC {
8
9class DiscordInterface {
10public:
11 virtual ~DiscordInterface() = default;
12
13 virtual void Pause() = 0;
14 virtual void Update() = 0;
15};
16
17class NullImpl : public DiscordInterface {
18public:
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
14namespace DiscordRPC {
15
16DiscordImpl::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
24DiscordImpl::~DiscordImpl() {
25 Discord_ClearPresence();
26 Discord_Shutdown();
27}
28
29void DiscordImpl::Pause() {
30 Discord_ClearPresence();
31}
32
33void 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
9namespace DiscordRPC {
10
11class DiscordImpl : public DiscordInterface {
12public:
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
83Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); 90Q_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
105static void ShowCalloutMessage(const QString& message, CalloutFlag flag) { 112void 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
120void GMainWindow::ShowCallouts() {}
121
122const int GMainWindow::max_recent_files_item; 128const int GMainWindow::max_recent_files_item;
123 129
124static void InitializeLogging() { 130static 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
185void GMainWindow::InitializeWidgets() { 194void 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
649void GMainWindow::ShutdownGame() { 663void 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
1152void GMainWindow::OnPauseGame() { 1173void GMainWindow::OnPauseGame() {
@@ -1161,6 +1182,20 @@ void GMainWindow::OnStopGame() {
1161 ShutdownGame(); 1182 ShutdownGame();
1162} 1183}
1163 1184
1185void 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 &gt; Configuration "
1194 "&gt; "
1195 "Web."));
1196 }
1197}
1198
1164void GMainWindow::ToggleFullscreen() { 1199void GMainWindow::ToggleFullscreen() {
1165 if (!emulation_running) { 1200 if (!emulation_running) {
1166 return; 1201 return;
@@ -1224,11 +1259,14 @@ void GMainWindow::ToggleWindowMode() {
1224void GMainWindow::OnConfigure() { 1259void 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
1484void 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
1450int main(int argc, char* argv[]) { 1501int 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
44namespace DiscordRPC {
45class DiscordInterface;
46}
47
44class GMainWindow : public QMainWindow { 48class 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
64signals: 70signals:
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>&amp;Help</string> 102 <string>&amp;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
143void Config::Reload() { 151void 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
204enable_telemetry = 204enable_telemetry =
205# Endpoint URL for submitting telemetry data 205# URL for Web API
206telemetry_endpoint_url = 206web_api_url = https://api.yuzu-emu.org
207# Endpoint URL to verify the username and token
208verify_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
211yuzu_username = 209yuzu_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
80int main(int argc, char** argv) { 81int 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}