Merge pull request #1332 from FearlessTobi/port-web-backend
Port web_service from Citra
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -34,3 +34,9 @@ | ||||
| [submodule "soundtouch"] | ||||
| 	path = externals/soundtouch | ||||
| 	url = https://github.com/citra-emu/ext-soundtouch.git | ||||
| [submodule "libressl"] | ||||
|     path = externals/libressl | ||||
|     url = https://github.com/citra-emu/ext-libressl-portable.git | ||||
| [submodule "discord-rpc"] | ||||
|     path = externals/discord-rpc | ||||
|     url = https://github.com/discordapp/discord-rpc.git | ||||
|   | ||||
| @@ -10,3 +10,7 @@ TRAVIS_JOB_ID | ||||
| TRAVIS_JOB_NUMBER | ||||
| TRAVIS_REPO_SLUG | ||||
| TRAVIS_TAG | ||||
|  | ||||
| # yuzu specific flags | ||||
| ENABLE_COMPATIBILITY_REPORTING | ||||
| USE_DISCORD_PRESENCE | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/bin/bash -ex | ||||
|  | ||||
| mkdir -p "$HOME/.ccache" | ||||
| 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 | ||||
| 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 | ||||
|   | ||||
| @@ -6,7 +6,7 @@ apt-get install --no-install-recommends -y build-essential git libqt5opengl5-dev | ||||
| cd /yuzu | ||||
|  | ||||
| mkdir build && cd build | ||||
| 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 | ||||
| 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 | ||||
| ninja | ||||
|  | ||||
| ccache -s | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export PATH="/usr/local/opt/ccache/libexec:$PATH" | ||||
|  | ||||
| mkdir build && cd build | ||||
| cmake --version | ||||
| cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON | ||||
| 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 | ||||
| make -j4 | ||||
|  | ||||
| ccache -s | ||||
|   | ||||
| @@ -15,10 +15,14 @@ CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 binaries" ON | ||||
| option(ENABLE_QT "Enable the Qt frontend" ON) | ||||
| CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF) | ||||
|  | ||||
| option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) | ||||
|  | ||||
| option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) | ||||
|  | ||||
| option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) | ||||
|  | ||||
| option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) | ||||
|  | ||||
| if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) | ||||
|     message(STATUS "Copying pre-commit hook") | ||||
|     file(COPY hooks/pre-commit | ||||
|   | ||||
| @@ -39,11 +39,12 @@ before_build: | ||||
|   - mkdir %BUILD_TYPE%_build | ||||
|   - cd %BUILD_TYPE%_build | ||||
|   - ps: | | ||||
|         $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING} | ||||
|         if ($env:BUILD_TYPE -eq 'msvc') { | ||||
|           # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning | ||||
|           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' | ||||
|           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' | ||||
|         } else { | ||||
|           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" | ||||
|           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" | ||||
|         } | ||||
|   - cd .. | ||||
|  | ||||
|   | ||||
							
								
								
									
										25
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							| @@ -70,3 +70,28 @@ if(ENABLE_CUBEB) | ||||
|     set(BUILD_TESTS OFF CACHE BOOL "") | ||||
|     add_subdirectory(cubeb EXCLUDE_FROM_ALL) | ||||
| endif() | ||||
|  | ||||
| # DiscordRPC | ||||
| if (USE_DISCORD_PRESENCE) | ||||
|     add_subdirectory(discord-rpc EXCLUDE_FROM_ALL) | ||||
|     target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) | ||||
| endif() | ||||
|  | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     # LibreSSL | ||||
|     set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") | ||||
|     add_subdirectory(libressl EXCLUDE_FROM_ALL) | ||||
|     target_include_directories(ssl INTERFACE ./libressl/include) | ||||
| 	target_compile_definitions(ssl PRIVATE -DHAVE_INET_NTOP) | ||||
|  | ||||
|     # lurlparser | ||||
|     add_subdirectory(lurlparser EXCLUDE_FROM_ALL) | ||||
|  | ||||
|     # httplib | ||||
|     add_library(httplib INTERFACE) | ||||
|     target_include_directories(httplib INTERFACE ./httplib) | ||||
|  | ||||
|     # JSON | ||||
|     add_library(json-headers INTERFACE) | ||||
|     target_include_directories(json-headers INTERFACE ./json) | ||||
| endif() | ||||
|   | ||||
							
								
								
									
										1
									
								
								externals/discord-rpc
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								externals/discord-rpc
									
									
									
									
										vendored
									
									
										Submodule
									
								
							 Submodule externals/discord-rpc added at e32d001809
									
								
							
							
								
								
									
										15
									
								
								externals/httplib/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								externals/httplib/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| From https://github.com/yhirose/cpp-httplib/commit/d9479bc0b12e8a1e8bce2d34da4feeef488581f3 | ||||
|  | ||||
| MIT License | ||||
|  | ||||
| === | ||||
|  | ||||
| cpp-httplib | ||||
|  | ||||
| A C++11 header-only HTTP library. | ||||
|  | ||||
| It's extremely easy to setup. Just include httplib.h file in your code! | ||||
|  | ||||
| Inspired by Sinatra and express. | ||||
|  | ||||
| © 2017 Yuji Hirose | ||||
							
								
								
									
										2344
									
								
								externals/httplib/httplib.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2344
									
								
								externals/httplib/httplib.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										9
									
								
								externals/json/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								externals/json/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| JSON for Modern C++ | ||||
| =================== | ||||
|  | ||||
| v3.1.2 | ||||
|  | ||||
| This is a mirror providing the single required header file. | ||||
|  | ||||
| The original repository can be found at: | ||||
| https://github.com/nlohmann/json/commit/d2dd27dc3b8472dbaa7d66f83619b3ebcd9185fe | ||||
							
								
								
									
										17300
									
								
								externals/json/json.hpp
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17300
									
								
								externals/json/json.hpp
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								externals/libressl
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								externals/libressl
									
									
									
									
										vendored
									
									
										Submodule
									
								
							 Submodule externals/libressl added at 7d01cb01cb
									
								
							
							
								
								
									
										8
									
								
								externals/lurlparser/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								externals/lurlparser/CMakeLists.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| add_library(lurlparser | ||||
|         LUrlParser.cpp | ||||
|         LUrlParser.h | ||||
| ) | ||||
|  | ||||
| create_target_directory_groups(lurlparser) | ||||
|  | ||||
| target_include_directories(lurlparser INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) | ||||
							
								
								
									
										265
									
								
								externals/lurlparser/LUrlParser.cpp
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								externals/lurlparser/LUrlParser.cpp
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,265 @@ | ||||
| /* | ||||
|  * Lightweight URL & URI parser (RFC 1738, RFC 3986) | ||||
|  * https://github.com/corporateshark/LUrlParser | ||||
|  * | ||||
|  * The MIT License (MIT) | ||||
|  * | ||||
|  * Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com) | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| #include "LUrlParser.h" | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
| #include <stdlib.h> | ||||
|  | ||||
| // check if the scheme name is valid | ||||
| static bool IsSchemeValid( const std::string& SchemeName ) | ||||
| { | ||||
|     for ( auto c : SchemeName  ) | ||||
|     { | ||||
|         if ( !isalpha( c ) && c != '+' && c != '-' && c != '.' ) return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool LUrlParser::clParseURL::GetPort( int* OutPort ) const | ||||
| { | ||||
|     if ( !IsValid() ) { return false; } | ||||
|  | ||||
|     int Port = atoi( m_Port.c_str() ); | ||||
|  | ||||
|     if ( Port <= 0 || Port > 65535 ) { return false; } | ||||
|  | ||||
|     if ( OutPort ) { *OutPort = Port; } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| // based on RFC 1738 and RFC 3986 | ||||
| LUrlParser::clParseURL LUrlParser::clParseURL::ParseURL( const std::string& URL ) | ||||
| { | ||||
|     LUrlParser::clParseURL Result; | ||||
|  | ||||
|     const char* CurrentString = URL.c_str(); | ||||
|  | ||||
|     /* | ||||
|      *	<scheme>:<scheme-specific-part> | ||||
|      *	<scheme> := [a-z\+\-\.]+ | ||||
|      *	For resiliency, programs interpreting URLs should treat upper case letters as equivalent to lower case in scheme names | ||||
|      */ | ||||
|  | ||||
|     // try to read scheme | ||||
|     { | ||||
|         const char* LocalString = strchr( CurrentString, ':' ); | ||||
|  | ||||
|         if ( !LocalString ) | ||||
|         { | ||||
|             return clParseURL( LUrlParserError_NoUrlCharacter ); | ||||
|         } | ||||
|  | ||||
|         // save the scheme name | ||||
|         Result.m_Scheme = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|         if ( !IsSchemeValid( Result.m_Scheme ) ) | ||||
|         { | ||||
|             return clParseURL( LUrlParserError_InvalidSchemeName ); | ||||
|         } | ||||
|  | ||||
|         // scheme should be lowercase | ||||
|         std::transform( Result.m_Scheme.begin(), Result.m_Scheme.end(), Result.m_Scheme.begin(), ::tolower ); | ||||
|  | ||||
|         // skip ':' | ||||
|         CurrentString = LocalString+1; | ||||
|     } | ||||
|  | ||||
|     /* | ||||
|      *	//<user>:<password>@<host>:<port>/<url-path> | ||||
|      *	any ":", "@" and "/" must be normalized | ||||
|      */ | ||||
|  | ||||
|     // skip "//" | ||||
|     if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash ); | ||||
|     if ( *CurrentString++ != '/' ) return clParseURL( LUrlParserError_NoDoubleSlash ); | ||||
|  | ||||
|     // check if the user name and password are specified | ||||
|     bool bHasUserName = false; | ||||
|  | ||||
|     const char* LocalString = CurrentString; | ||||
|  | ||||
|     while ( *LocalString ) | ||||
|     { | ||||
|         if ( *LocalString == '@' ) | ||||
|         { | ||||
|             // user name and password are specified | ||||
|             bHasUserName = true; | ||||
|             break; | ||||
|         } | ||||
|         else if ( *LocalString == '/' ) | ||||
|         { | ||||
|             // end of <host>:<port> specification | ||||
|             bHasUserName = false; | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         LocalString++; | ||||
|     } | ||||
|  | ||||
|     // user name and password | ||||
|     LocalString = CurrentString; | ||||
|  | ||||
|     if ( bHasUserName ) | ||||
|     { | ||||
|         // read user name | ||||
|         while ( *LocalString && *LocalString != ':' && *LocalString != '@' ) LocalString++; | ||||
|  | ||||
|         Result.m_UserName = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|         // proceed with the current pointer | ||||
|         CurrentString = LocalString; | ||||
|  | ||||
|         if ( *CurrentString == ':' ) | ||||
|         { | ||||
|             // skip ':' | ||||
|             CurrentString++; | ||||
|  | ||||
|             // read password | ||||
|             LocalString = CurrentString; | ||||
|  | ||||
|             while ( *LocalString && *LocalString != '@' ) LocalString++; | ||||
|  | ||||
|             Result.m_Password = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|             CurrentString = LocalString; | ||||
|         } | ||||
|  | ||||
|         // skip '@' | ||||
|         if ( *CurrentString != '@' ) | ||||
|         { | ||||
|             return clParseURL( LUrlParserError_NoAtSign ); | ||||
|         } | ||||
|  | ||||
|         CurrentString++; | ||||
|     } | ||||
|  | ||||
|     bool bHasBracket = ( *CurrentString == '[' ); | ||||
|  | ||||
|     // go ahead, read the host name | ||||
|     LocalString = CurrentString; | ||||
|  | ||||
|     while ( *LocalString ) | ||||
|     { | ||||
|         if ( bHasBracket && *LocalString == ']' ) | ||||
|         { | ||||
|             // end of IPv6 address | ||||
|             LocalString++; | ||||
|             break; | ||||
|         } | ||||
|         else if ( !bHasBracket && ( *LocalString == ':' || *LocalString == '/' ) ) | ||||
|         { | ||||
|             // port number is specified | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         LocalString++; | ||||
|     } | ||||
|  | ||||
|     Result.m_Host = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|     CurrentString = LocalString; | ||||
|  | ||||
|     // is port number specified? | ||||
|     if ( *CurrentString == ':' ) | ||||
|     { | ||||
|         CurrentString++; | ||||
|  | ||||
|         // read port number | ||||
|         LocalString = CurrentString; | ||||
|  | ||||
|         while ( *LocalString && *LocalString != '/' ) LocalString++; | ||||
|  | ||||
|         Result.m_Port = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|         CurrentString = LocalString; | ||||
|     } | ||||
|  | ||||
|     // end of string | ||||
|     if ( !*CurrentString ) | ||||
|     { | ||||
|         Result.m_ErrorCode = LUrlParserError_Ok; | ||||
|  | ||||
|         return Result; | ||||
|     } | ||||
|  | ||||
|     // skip '/' | ||||
|     if ( *CurrentString != '/' ) | ||||
|     { | ||||
|         return clParseURL( LUrlParserError_NoSlash ); | ||||
|     } | ||||
|  | ||||
|     CurrentString++; | ||||
|  | ||||
|     // parse the path | ||||
|     LocalString = CurrentString; | ||||
|  | ||||
|     while ( *LocalString && *LocalString != '#' && *LocalString != '?' ) LocalString++; | ||||
|  | ||||
|     Result.m_Path = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|     CurrentString = LocalString; | ||||
|  | ||||
|     // check for query | ||||
|     if ( *CurrentString == '?' ) | ||||
|     { | ||||
|         // skip '?' | ||||
|         CurrentString++; | ||||
|  | ||||
|         // read query | ||||
|         LocalString = CurrentString; | ||||
|  | ||||
|         while ( *LocalString && *LocalString != '#' ) LocalString++; | ||||
|  | ||||
|         Result.m_Query = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|         CurrentString = LocalString; | ||||
|     } | ||||
|  | ||||
|     // check for fragment | ||||
|     if ( *CurrentString == '#' ) | ||||
|     { | ||||
|         // skip '#' | ||||
|         CurrentString++; | ||||
|  | ||||
|         // read fragment | ||||
|         LocalString = CurrentString; | ||||
|  | ||||
|         while ( *LocalString ) LocalString++; | ||||
|  | ||||
|         Result.m_Fragment = std::string( CurrentString, LocalString - CurrentString ); | ||||
|  | ||||
|         CurrentString = LocalString; | ||||
|     } | ||||
|  | ||||
|     Result.m_ErrorCode = LUrlParserError_Ok; | ||||
|  | ||||
|     return Result; | ||||
| } | ||||
							
								
								
									
										78
									
								
								externals/lurlparser/LUrlParser.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								externals/lurlparser/LUrlParser.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| /* | ||||
|  * Lightweight URL & URI parser (RFC 1738, RFC 3986) | ||||
|  * https://github.com/corporateshark/LUrlParser | ||||
|  * | ||||
|  * The MIT License (MIT) | ||||
|  * | ||||
|  * Copyright (C) 2015 Sergey Kosarevsky (sk@linderdaum.com) | ||||
|  * | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| namespace LUrlParser | ||||
| { | ||||
| enum LUrlParserError | ||||
| { | ||||
|     LUrlParserError_Ok = 0, | ||||
|     LUrlParserError_Uninitialized = 1, | ||||
|     LUrlParserError_NoUrlCharacter = 2, | ||||
|     LUrlParserError_InvalidSchemeName = 3, | ||||
|     LUrlParserError_NoDoubleSlash = 4, | ||||
|     LUrlParserError_NoAtSign = 5, | ||||
|     LUrlParserError_UnexpectedEndOfLine = 6, | ||||
|     LUrlParserError_NoSlash = 7, | ||||
| }; | ||||
|  | ||||
| class clParseURL | ||||
| { | ||||
| public: | ||||
|     LUrlParserError m_ErrorCode; | ||||
|     std::string m_Scheme; | ||||
|     std::string m_Host; | ||||
|     std::string m_Port; | ||||
|     std::string m_Path; | ||||
|     std::string m_Query; | ||||
|     std::string m_Fragment; | ||||
|     std::string m_UserName; | ||||
|     std::string m_Password; | ||||
|  | ||||
|     clParseURL() | ||||
|             : m_ErrorCode( LUrlParserError_Uninitialized ) | ||||
|     {} | ||||
|  | ||||
|     /// return 'true' if the parsing was successful | ||||
|     bool IsValid() const { return m_ErrorCode == LUrlParserError_Ok; } | ||||
|  | ||||
|     /// helper to convert the port number to int, return 'true' if the port is valid (within the 0..65535 range) | ||||
|     bool GetPort( int* OutPort ) const; | ||||
|  | ||||
|     /// parse the URL | ||||
|     static clParseURL ParseURL( const std::string& URL ); | ||||
|  | ||||
| private: | ||||
|     explicit clParseURL( LUrlParserError ErrorCode ) | ||||
|             : m_ErrorCode( ErrorCode ) | ||||
|     {} | ||||
| }; | ||||
|  | ||||
| } // namespace LUrlParser | ||||
							
								
								
									
										19
									
								
								externals/lurlparser/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								externals/lurlparser/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| From https://github.com/corporateshark/LUrlParser/commit/455d5e2d27e3946f11ad0328fee9ee2628e6a8e2 | ||||
|  | ||||
| MIT License | ||||
|  | ||||
| === | ||||
|  | ||||
| Lightweight URL & URI parser (RFC 1738, RFC 3986) | ||||
|  | ||||
| (C) Sergey Kosarevsky, 2015 | ||||
|  | ||||
| @corporateshark sk@linderdaum.com | ||||
|  | ||||
| http://www.linderdaum.com | ||||
|  | ||||
| http://blog.linderdaum.com | ||||
|  | ||||
| ============================= | ||||
|  | ||||
| A tiny and lightweight URL & URI parser (RFC 1738, RFC 3986) written in C++. | ||||
| @@ -13,3 +13,6 @@ endif() | ||||
| if (ENABLE_QT) | ||||
|     add_subdirectory(yuzu) | ||||
| endif() | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     add_subdirectory(web_service) | ||||
| endif() | ||||
|   | ||||
| @@ -41,6 +41,8 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOU | ||||
| add_library(common STATIC | ||||
|     alignment.h | ||||
|     assert.h | ||||
|     detached_tasks.cpp | ||||
|     detached_tasks.h | ||||
|     bit_field.h | ||||
|     bit_set.h | ||||
|     cityhash.cpp | ||||
| @@ -87,6 +89,7 @@ add_library(common STATIC | ||||
|     timer.cpp | ||||
|     timer.h | ||||
|     vector_math.h | ||||
|     web_result.h | ||||
| ) | ||||
|  | ||||
| if(ARCHITECTURE_x86_64) | ||||
|   | ||||
							
								
								
									
										41
									
								
								src/common/detached_tasks.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/common/detached_tasks.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // Copyright 2018 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <thread> | ||||
| #include "common/assert.h" | ||||
| #include "common/detached_tasks.h" | ||||
|  | ||||
| namespace Common { | ||||
|  | ||||
| DetachedTasks* DetachedTasks::instance = nullptr; | ||||
|  | ||||
| DetachedTasks::DetachedTasks() { | ||||
|     ASSERT(instance == nullptr); | ||||
|     instance = this; | ||||
| } | ||||
|  | ||||
| void DetachedTasks::WaitForAllTasks() { | ||||
|     std::unique_lock<std::mutex> lock(mutex); | ||||
|     cv.wait(lock, [this]() { return count == 0; }); | ||||
| } | ||||
|  | ||||
| DetachedTasks::~DetachedTasks() { | ||||
|     std::unique_lock<std::mutex> lock(mutex); | ||||
|     ASSERT(count == 0); | ||||
|     instance = nullptr; | ||||
| } | ||||
|  | ||||
| void DetachedTasks::AddTask(std::function<void()> task) { | ||||
|     std::unique_lock<std::mutex> lock(instance->mutex); | ||||
|     ++instance->count; | ||||
|     std::thread([task{std::move(task)}]() { | ||||
|         task(); | ||||
|         std::unique_lock<std::mutex> lock(instance->mutex); | ||||
|         --instance->count; | ||||
|         std::notify_all_at_thread_exit(instance->cv, std::move(lock)); | ||||
|     }) | ||||
|         .detach(); | ||||
| } | ||||
|  | ||||
| } // namespace Common | ||||
							
								
								
									
										40
									
								
								src/common/detached_tasks.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/common/detached_tasks.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // Copyright 2018 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <condition_variable> | ||||
| #include <functional> | ||||
|  | ||||
| namespace Common { | ||||
|  | ||||
| /** | ||||
|  * A background manager which ensures that all detached task is finished before program exits. | ||||
|  * | ||||
|  * Some tasks, telemetry submission for example, prefer executing asynchronously and don't care | ||||
|  * about the result. These tasks are suitable for std::thread::detach(). However, this is unsafe if | ||||
|  * the task is launched just before the program exits (which is a common case for telemetry), so we | ||||
|  * need to block on these tasks on program exit. | ||||
|  * | ||||
|  * To make detached task safe, a single DetachedTasks object should be placed in the main(), and | ||||
|  * call WaitForAllTasks() after all program execution but before global/static variable destruction. | ||||
|  * Any potentially unsafe detached task should be executed via DetachedTasks::AddTask. | ||||
|  */ | ||||
| class DetachedTasks { | ||||
| public: | ||||
|     DetachedTasks(); | ||||
|     ~DetachedTasks(); | ||||
|     void WaitForAllTasks(); | ||||
|  | ||||
|     static void AddTask(std::function<void()> task); | ||||
|  | ||||
| private: | ||||
|     static DetachedTasks* instance; | ||||
|  | ||||
|     std::condition_variable cv; | ||||
|     std::mutex mutex; | ||||
|     int count = 0; | ||||
| }; | ||||
|  | ||||
| } // namespace Common | ||||
							
								
								
									
										24
									
								
								src/common/web_result.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/common/web_result.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // Copyright 2018 yuzu Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <string> | ||||
|  | ||||
| namespace Common { | ||||
| struct WebResult { | ||||
|     enum class Code : u32 { | ||||
|         Success, | ||||
|         InvalidURL, | ||||
|         CredentialsMissing, | ||||
|         LibError, | ||||
|         HttpError, | ||||
|         WrongContent, | ||||
|         NoWebservice, | ||||
|     }; | ||||
|     Code result_code; | ||||
|     std::string result_string; | ||||
|     std::string returned_data; | ||||
| }; | ||||
| } // namespace Common | ||||
| @@ -396,6 +396,10 @@ create_target_directory_groups(core) | ||||
|  | ||||
| target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) | ||||
| target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives) | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     add_definitions(-DENABLE_WEB_SERVICE) | ||||
|     target_link_libraries(core PUBLIC json-headers web_service) | ||||
| endif() | ||||
|  | ||||
| if (ARCHITECTURE_x86_64) | ||||
|     target_sources(core PRIVATE | ||||
|   | ||||
| @@ -155,6 +155,12 @@ struct Values { | ||||
|     // Debugging | ||||
|     bool use_gdbstub; | ||||
|     u16 gdbstub_port; | ||||
|  | ||||
|     // WebService | ||||
|     bool enable_telemetry; | ||||
|     std::string web_api_url; | ||||
|     std::string yuzu_username; | ||||
|     std::string yuzu_token; | ||||
| } extern values; | ||||
|  | ||||
| void Apply(); | ||||
|   | ||||
| @@ -6,6 +6,8 @@ | ||||
| #include "common/common_types.h" | ||||
| #include "common/file_util.h" | ||||
|  | ||||
| #include <mbedtls/ctr_drbg.h> | ||||
| #include <mbedtls/entropy.h> | ||||
| #include "core/core.h" | ||||
| #include "core/file_sys/control_metadata.h" | ||||
| #include "core/file_sys/patch_manager.h" | ||||
| @@ -13,10 +15,31 @@ | ||||
| #include "core/settings.h" | ||||
| #include "core/telemetry_session.h" | ||||
|  | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
| #include "web_service/telemetry_json.h" | ||||
| #include "web_service/verify_login.h" | ||||
| #endif | ||||
|  | ||||
| namespace Core { | ||||
|  | ||||
| static u64 GenerateTelemetryId() { | ||||
|     u64 telemetry_id{}; | ||||
|  | ||||
|     mbedtls_entropy_context entropy; | ||||
|     mbedtls_entropy_init(&entropy); | ||||
|     mbedtls_ctr_drbg_context ctr_drbg; | ||||
|     std::string personalization = "yuzu Telemetry ID"; | ||||
|  | ||||
|     mbedtls_ctr_drbg_init(&ctr_drbg); | ||||
|     ASSERT(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, | ||||
|                                  reinterpret_cast<const unsigned char*>(personalization.c_str()), | ||||
|                                  personalization.size()) == 0); | ||||
|     ASSERT(mbedtls_ctr_drbg_random(&ctr_drbg, reinterpret_cast<unsigned char*>(&telemetry_id), | ||||
|                                    sizeof(u64)) == 0); | ||||
|  | ||||
|     mbedtls_ctr_drbg_free(&ctr_drbg); | ||||
|     mbedtls_entropy_free(&entropy); | ||||
|  | ||||
|     return telemetry_id; | ||||
| } | ||||
|  | ||||
| @@ -25,14 +48,21 @@ u64 GetTelemetryId() { | ||||
|     const std::string filename{FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + | ||||
|                                "telemetry_id"}; | ||||
|  | ||||
|     if (FileUtil::Exists(filename)) { | ||||
|     bool generate_new_id = !FileUtil::Exists(filename); | ||||
|     if (!generate_new_id) { | ||||
|         FileUtil::IOFile file(filename, "rb"); | ||||
|         if (!file.IsOpen()) { | ||||
|             LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); | ||||
|             return {}; | ||||
|         } | ||||
|         file.ReadBytes(&telemetry_id, sizeof(u64)); | ||||
|     } else { | ||||
|         if (telemetry_id == 0) { | ||||
|             LOG_ERROR(Frontend, "telemetry_id is 0. Generating a new one.", telemetry_id); | ||||
|             generate_new_id = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (generate_new_id) { | ||||
|         FileUtil::IOFile file(filename, "wb"); | ||||
|         if (!file.IsOpen()) { | ||||
|             LOG_ERROR(Core, "failed to open telemetry_id: {}", filename); | ||||
| @@ -59,23 +89,20 @@ u64 RegenerateTelemetryId() { | ||||
|     return new_telemetry_id; | ||||
| } | ||||
|  | ||||
| std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func) { | ||||
| bool VerifyLogin(const std::string& username, const std::string& token) { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|     return WebService::VerifyLogin(username, token, Settings::values.verify_endpoint_url, func); | ||||
|     return WebService::VerifyLogin(Settings::values.web_api_url, username, token); | ||||
| #else | ||||
|     return std::async(std::launch::async, [func{std::move(func)}]() { | ||||
|         func(); | ||||
|         return false; | ||||
|     }); | ||||
|     return false; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| TelemetrySession::TelemetrySession() { | ||||
| #ifdef ENABLE_WEB_SERVICE | ||||
|     if (Settings::values.enable_telemetry) { | ||||
|         backend = std::make_unique<WebService::TelemetryJson>( | ||||
|             Settings::values.telemetry_endpoint_url, Settings::values.yuzu_username, | ||||
|             Settings::values.yuzu_token); | ||||
|         backend = std::make_unique<WebService::TelemetryJson>(Settings::values.web_api_url, | ||||
|                                                               Settings::values.yuzu_username, | ||||
|                                                               Settings::values.yuzu_token); | ||||
|     } else { | ||||
|         backend = std::make_unique<Telemetry::NullVisitor>(); | ||||
|     } | ||||
| @@ -94,7 +121,8 @@ TelemetrySession::TelemetrySession() { | ||||
|     u64 program_id{}; | ||||
|     const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)}; | ||||
|     if (res == Loader::ResultStatus::Success) { | ||||
|         AddField(Telemetry::FieldType::Session, "ProgramId", program_id); | ||||
|         const std::string formatted_program_id{fmt::format("{:016X}", program_id)}; | ||||
|         AddField(Telemetry::FieldType::Session, "ProgramId", formatted_program_id); | ||||
|  | ||||
|         std::string name; | ||||
|         System::GetInstance().GetAppLoader().ReadTitle(name); | ||||
|   | ||||
| @@ -4,7 +4,6 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <future> | ||||
| #include <memory> | ||||
| #include "common/telemetry.h" | ||||
|  | ||||
| @@ -31,6 +30,8 @@ public: | ||||
|         field_collection.AddField(type, name, std::move(value)); | ||||
|     } | ||||
|  | ||||
|     static void FinalizeAsyncJob(); | ||||
|  | ||||
| private: | ||||
|     Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session | ||||
|     std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields | ||||
| @@ -55,6 +56,6 @@ u64 RegenerateTelemetryId(); | ||||
|  * @param func A function that gets exectued when the verification is finished | ||||
|  * @returns Future with bool indicating whether the verification succeeded | ||||
|  */ | ||||
| std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func); | ||||
| bool VerifyLogin(const std::string& username, const std::string& token); | ||||
|  | ||||
| } // namespace Core | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/web_service/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/web_service/CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| add_library(web_service STATIC | ||||
|     telemetry_json.cpp | ||||
|     telemetry_json.h | ||||
|     verify_login.cpp | ||||
|     verify_login.h | ||||
|     web_backend.cpp | ||||
|     web_backend.h | ||||
| ) | ||||
|  | ||||
| create_target_directory_groups(web_service) | ||||
|  | ||||
| get_directory_property(OPENSSL_LIBS | ||||
|         DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl | ||||
|         DEFINITION OPENSSL_LIBS) | ||||
| target_compile_definitions(web_service PUBLIC -DCPPHTTPLIB_OPENSSL_SUPPORT) | ||||
| target_link_libraries(web_service PRIVATE common json-headers ${OPENSSL_LIBS} httplib lurlparser) | ||||
							
								
								
									
										99
									
								
								src/web_service/telemetry_json.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/web_service/telemetry_json.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <thread> | ||||
| #include "common/assert.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "web_service/telemetry_json.h" | ||||
| #include "web_service/web_backend.h" | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| TelemetryJson::TelemetryJson(const std::string& host, const std::string& username, | ||||
|                              const std::string& token) | ||||
|     : host(std::move(host)), username(std::move(username)), token(std::move(token)) {} | ||||
| TelemetryJson::~TelemetryJson() = default; | ||||
|  | ||||
| template <class T> | ||||
| void TelemetryJson::Serialize(Telemetry::FieldType type, const std::string& name, T value) { | ||||
|     sections[static_cast<u8>(type)][name] = value; | ||||
| } | ||||
|  | ||||
| void TelemetryJson::SerializeSection(Telemetry::FieldType type, const std::string& name) { | ||||
|     TopSection()[name] = sections[static_cast<unsigned>(type)]; | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<bool>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<double>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<float>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<u8>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<u16>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<u32>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<u64>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<s8>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<s16>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<s32>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<s64>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<std::string>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<const char*>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), std::string(field.GetValue())); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Visit(const Telemetry::Field<std::chrono::microseconds>& field) { | ||||
|     Serialize(field.GetType(), field.GetName(), field.GetValue().count()); | ||||
| } | ||||
|  | ||||
| void TelemetryJson::Complete() { | ||||
|     SerializeSection(Telemetry::FieldType::App, "App"); | ||||
|     SerializeSection(Telemetry::FieldType::Session, "Session"); | ||||
|     SerializeSection(Telemetry::FieldType::Performance, "Performance"); | ||||
|     SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback"); | ||||
|     SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); | ||||
|     SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); | ||||
|  | ||||
|     auto content = TopSection().dump(); | ||||
|     // Send the telemetry async but don't handle the errors since they were written to the log | ||||
|     Common::DetachedTasks::AddTask( | ||||
|         [host{this->host}, username{this->username}, token{this->token}, content]() { | ||||
|             Client{host, username, token}.PostJson("/telemetry", content, true); | ||||
|         }); | ||||
| } | ||||
|  | ||||
| } // namespace WebService | ||||
							
								
								
									
										58
									
								
								src/web_service/telemetry_json.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/web_service/telemetry_json.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #include <string> | ||||
| #include <json.hpp> | ||||
| #include "common/telemetry.h" | ||||
| #include "common/web_result.h" | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| /** | ||||
|  * Implementation of VisitorInterface that serialized telemetry into JSON, and submits it to the | ||||
|  * yuzu web service | ||||
|  */ | ||||
| class TelemetryJson : public Telemetry::VisitorInterface { | ||||
| public: | ||||
|     TelemetryJson(const std::string& host, const std::string& username, const std::string& token); | ||||
|     ~TelemetryJson(); | ||||
|  | ||||
|     void Visit(const Telemetry::Field<bool>& field) override; | ||||
|     void Visit(const Telemetry::Field<double>& field) override; | ||||
|     void Visit(const Telemetry::Field<float>& field) override; | ||||
|     void Visit(const Telemetry::Field<u8>& field) override; | ||||
|     void Visit(const Telemetry::Field<u16>& field) override; | ||||
|     void Visit(const Telemetry::Field<u32>& field) override; | ||||
|     void Visit(const Telemetry::Field<u64>& field) override; | ||||
|     void Visit(const Telemetry::Field<s8>& field) override; | ||||
|     void Visit(const Telemetry::Field<s16>& field) override; | ||||
|     void Visit(const Telemetry::Field<s32>& field) override; | ||||
|     void Visit(const Telemetry::Field<s64>& field) override; | ||||
|     void Visit(const Telemetry::Field<std::string>& field) override; | ||||
|     void Visit(const Telemetry::Field<const char*>& field) override; | ||||
|     void Visit(const Telemetry::Field<std::chrono::microseconds>& field) override; | ||||
|  | ||||
|     void Complete() override; | ||||
|  | ||||
| private: | ||||
|     nlohmann::json& TopSection() { | ||||
|         return sections[static_cast<u8>(Telemetry::FieldType::None)]; | ||||
|     } | ||||
|  | ||||
|     template <class T> | ||||
|     void Serialize(Telemetry::FieldType type, const std::string& name, T value); | ||||
|  | ||||
|     void SerializeSection(Telemetry::FieldType type, const std::string& name); | ||||
|  | ||||
|     nlohmann::json output; | ||||
|     std::array<nlohmann::json, 7> sections; | ||||
|     std::string host; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
| }; | ||||
|  | ||||
| } // namespace WebService | ||||
							
								
								
									
										27
									
								
								src/web_service/verify_login.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/web_service/verify_login.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <json.hpp> | ||||
| #include "web_service/verify_login.h" | ||||
| #include "web_service/web_backend.h" | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) { | ||||
|     Client client(host, username, token); | ||||
|     auto reply = client.GetJson("/profile", false).returned_data; | ||||
|     if (reply.empty()) { | ||||
|         return false; | ||||
|     } | ||||
|     nlohmann::json json = nlohmann::json::parse(reply); | ||||
|     const auto iter = json.find("username"); | ||||
|  | ||||
|     if (iter == json.end()) { | ||||
|         return username.empty(); | ||||
|     } | ||||
|  | ||||
|     return username == *iter; | ||||
| } | ||||
|  | ||||
| } // namespace WebService | ||||
							
								
								
									
										22
									
								
								src/web_service/verify_login.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/web_service/verify_login.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <functional> | ||||
| #include <future> | ||||
| #include <string> | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| /** | ||||
|  * Checks if username and token is valid | ||||
|  * @param host the web API URL | ||||
|  * @param username yuzu username to use for authentication. | ||||
|  * @param token yuzu token to use for authentication. | ||||
|  * @returns a bool indicating whether the verification succeeded | ||||
|  */ | ||||
| bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token); | ||||
|  | ||||
| } // namespace WebService | ||||
							
								
								
									
										149
									
								
								src/web_service/web_backend.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/web_service/web_backend.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <cstdlib> | ||||
| #include <string> | ||||
| #include <thread> | ||||
| #include <LUrlParser.h> | ||||
| #include "common/logging/log.h" | ||||
| #include "common/web_result.h" | ||||
| #include "core/settings.h" | ||||
| #include "web_service/web_backend.h" | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| constexpr std::array<const char, 1> API_VERSION{'1'}; | ||||
|  | ||||
| constexpr u32 HTTP_PORT = 80; | ||||
| constexpr u32 HTTPS_PORT = 443; | ||||
|  | ||||
| constexpr u32 TIMEOUT_SECONDS = 30; | ||||
|  | ||||
| Client::JWTCache Client::jwt_cache{}; | ||||
|  | ||||
| Client::Client(const std::string& host, const std::string& username, const std::string& token) | ||||
|     : host(host), username(username), token(token) { | ||||
|     std::lock_guard<std::mutex> lock(jwt_cache.mutex); | ||||
|     if (username == jwt_cache.username && token == jwt_cache.token) { | ||||
|         jwt = jwt_cache.jwt; | ||||
|     } | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, | ||||
|                                       const std::string& data, const std::string& jwt, | ||||
|                                       const std::string& username, const std::string& token) { | ||||
|     if (cli == nullptr) { | ||||
|         auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); | ||||
|         int port; | ||||
|         if (parsedUrl.m_Scheme == "http") { | ||||
|             if (!parsedUrl.GetPort(&port)) { | ||||
|                 port = HTTP_PORT; | ||||
|             } | ||||
|             cli = | ||||
|                 std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS); | ||||
|         } else if (parsedUrl.m_Scheme == "https") { | ||||
|             if (!parsedUrl.GetPort(&port)) { | ||||
|                 port = HTTPS_PORT; | ||||
|             } | ||||
|             cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port, | ||||
|                                                        TIMEOUT_SECONDS); | ||||
|         } else { | ||||
|             LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme); | ||||
|             return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"}; | ||||
|         } | ||||
|     } | ||||
|     if (cli == nullptr) { | ||||
|         LOG_ERROR(WebService, "Invalid URL {}", host + path); | ||||
|         return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; | ||||
|     } | ||||
|  | ||||
|     httplib::Headers params; | ||||
|     if (!jwt.empty()) { | ||||
|         params = { | ||||
|             {std::string("Authorization"), fmt::format("Bearer {}", jwt)}, | ||||
|         }; | ||||
|     } else if (!username.empty()) { | ||||
|         params = { | ||||
|             {std::string("x-username"), username}, | ||||
|             {std::string("x-token"), token}, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     params.emplace(std::string("api-version"), std::string(API_VERSION.begin(), API_VERSION.end())); | ||||
|     if (method != "GET") { | ||||
|         params.emplace(std::string("Content-Type"), std::string("application/json")); | ||||
|     }; | ||||
|  | ||||
|     httplib::Request request; | ||||
|     request.method = method; | ||||
|     request.path = path; | ||||
|     request.headers = params; | ||||
|     request.body = data; | ||||
|  | ||||
|     httplib::Response response; | ||||
|  | ||||
|     if (!cli->send(request, response)) { | ||||
|         LOG_ERROR(WebService, "{} to {} returned null", method, host + path); | ||||
|         return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; | ||||
|     } | ||||
|  | ||||
|     if (response.status >= 400) { | ||||
|         LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, | ||||
|                   response.status); | ||||
|         return Common::WebResult{Common::WebResult::Code::HttpError, | ||||
|                                  std::to_string(response.status)}; | ||||
|     } | ||||
|  | ||||
|     auto content_type = response.headers.find("content-type"); | ||||
|  | ||||
|     if (content_type == response.headers.end()) { | ||||
|         LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); | ||||
|         return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; | ||||
|     } | ||||
|  | ||||
|     if (content_type->second.find("application/json") == std::string::npos && | ||||
|         content_type->second.find("text/html; charset=utf-8") == std::string::npos) { | ||||
|         LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, | ||||
|                   content_type->second); | ||||
|         return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; | ||||
|     } | ||||
|     return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; | ||||
| } | ||||
|  | ||||
| void Client::UpdateJWT() { | ||||
|     if (!username.empty() && !token.empty()) { | ||||
|         auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); | ||||
|         if (result.result_code != Common::WebResult::Code::Success) { | ||||
|             LOG_ERROR(WebService, "UpdateJWT failed"); | ||||
|         } else { | ||||
|             std::lock_guard<std::mutex> lock(jwt_cache.mutex); | ||||
|             jwt_cache.username = username; | ||||
|             jwt_cache.token = token; | ||||
|             jwt_cache.jwt = jwt = result.returned_data; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, | ||||
|                                       const std::string& data, bool allow_anonymous) { | ||||
|     if (jwt.empty()) { | ||||
|         UpdateJWT(); | ||||
|     } | ||||
|  | ||||
|     if (jwt.empty() && !allow_anonymous) { | ||||
|         LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); | ||||
|         return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"}; | ||||
|     } | ||||
|  | ||||
|     auto result = GenericJson(method, path, data, jwt); | ||||
|     if (result.result_string == "401") { | ||||
|         // Try again with new JWT | ||||
|         UpdateJWT(); | ||||
|         result = GenericJson(method, path, data, jwt); | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| } // namespace WebService | ||||
							
								
								
									
										92
									
								
								src/web_service/web_backend.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/web_service/web_backend.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <functional> | ||||
| #include <mutex> | ||||
| #include <string> | ||||
| #include <tuple> | ||||
| #include <httplib.h> | ||||
| #include "common/common_types.h" | ||||
| #include "common/web_result.h" | ||||
|  | ||||
| namespace httplib { | ||||
| class Client; | ||||
| } | ||||
|  | ||||
| namespace WebService { | ||||
|  | ||||
| class Client { | ||||
| public: | ||||
|     Client(const std::string& host, const std::string& username, const std::string& token); | ||||
|  | ||||
|     /** | ||||
|      * Posts JSON to the specified path. | ||||
|      * @param path the URL segment after the host address. | ||||
|      * @param data String of JSON data to use for the body of the POST request. | ||||
|      * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult PostJson(const std::string& path, const std::string& data, | ||||
|                                bool allow_anonymous) { | ||||
|         return GenericJson("POST", path, data, allow_anonymous); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets JSON from the specified path. | ||||
|      * @param path the URL segment after the host address. | ||||
|      * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult GetJson(const std::string& path, bool allow_anonymous) { | ||||
|         return GenericJson("GET", path, "", allow_anonymous); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Deletes JSON to the specified path. | ||||
|      * @param path the URL segment after the host address. | ||||
|      * @param data String of JSON data to use for the body of the DELETE request. | ||||
|      * @param allow_anonymous If true, allow anonymous unauthenticated requests. | ||||
|      * @return the result of the request. | ||||
|      */ | ||||
|     Common::WebResult DeleteJson(const std::string& path, const std::string& data, | ||||
|                                  bool allow_anonymous) { | ||||
|         return GenericJson("DELETE", path, data, allow_anonymous); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     /// A generic function handles POST, GET and DELETE request together | ||||
|     Common::WebResult GenericJson(const std::string& method, const std::string& path, | ||||
|                                   const std::string& data, bool allow_anonymous); | ||||
|  | ||||
|     /** | ||||
|      * A generic function with explicit authentication method specified | ||||
|      * JWT is used if the jwt parameter is not empty | ||||
|      * username + token is used if jwt is empty but username and token are not empty | ||||
|      * anonymous if all of jwt, username and token are empty | ||||
|      */ | ||||
|     Common::WebResult GenericJson(const std::string& method, const std::string& path, | ||||
|                                   const std::string& data, const std::string& jwt = "", | ||||
|                                   const std::string& username = "", const std::string& token = ""); | ||||
|  | ||||
|     // Retrieve a new JWT from given username and token | ||||
|     void UpdateJWT(); | ||||
|  | ||||
|     std::string host; | ||||
|     std::string username; | ||||
|     std::string token; | ||||
|     std::string jwt; | ||||
|     std::unique_ptr<httplib::Client> cli; | ||||
|  | ||||
|     struct JWTCache { | ||||
|         std::mutex mutex; | ||||
|         std::string username; | ||||
|         std::string token; | ||||
|         std::string jwt; | ||||
|     }; | ||||
|     static JWTCache jwt_cache; | ||||
| }; | ||||
|  | ||||
| } // namespace WebService | ||||
| @@ -29,6 +29,8 @@ add_executable(yuzu | ||||
|     configuration/configure_input.h | ||||
|     configuration/configure_system.cpp | ||||
|     configuration/configure_system.h | ||||
|     configuration/configure_web.cpp | ||||
|     configuration/configure_web.h | ||||
|     debugger/graphics/graphics_breakpoint_observer.cpp | ||||
|     debugger/graphics/graphics_breakpoint_observer.h | ||||
|     debugger/graphics/graphics_breakpoints.cpp | ||||
| @@ -42,6 +44,7 @@ add_executable(yuzu | ||||
|     debugger/profiler.h | ||||
|     debugger/wait_tree.cpp | ||||
|     debugger/wait_tree.h | ||||
|     discord.h | ||||
|     game_list.cpp | ||||
|     game_list.h | ||||
|     game_list_p.h | ||||
| @@ -57,6 +60,8 @@ add_executable(yuzu | ||||
|     util/spinbox.h | ||||
|     util/util.cpp | ||||
|     util/util.h | ||||
|     compatdb.cpp | ||||
|     compatdb.h | ||||
|     yuzu.rc | ||||
| ) | ||||
|  | ||||
| @@ -70,8 +75,10 @@ set(UIS | ||||
|     configuration/configure_graphics.ui | ||||
|     configuration/configure_input.ui | ||||
|     configuration/configure_system.ui | ||||
|     configuration/configure_web.ui | ||||
|     hotkeys.ui | ||||
|     main.ui | ||||
|     compatdb.ui | ||||
| ) | ||||
|  | ||||
| file(GLOB COMPAT_LIST | ||||
| @@ -113,6 +120,19 @@ target_link_libraries(yuzu PRIVATE common core input_common video_core) | ||||
| target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets) | ||||
| target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) | ||||
|  | ||||
| if (YUZU_ENABLE_COMPATIBILITY_REPORTING) | ||||
|     add_definitions(-DYUZU_ENABLE_COMPATIBILITY_REPORTING) | ||||
| endif() | ||||
|  | ||||
| if (USE_DISCORD_PRESENCE) | ||||
|     target_sources(yuzu PUBLIC | ||||
|         discord_impl.cpp | ||||
|         discord_impl.h | ||||
|     ) | ||||
|     target_link_libraries(yuzu PRIVATE discord-rpc) | ||||
|     target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) | ||||
| endif() | ||||
|  | ||||
| if(UNIX AND NOT APPLE) | ||||
|     install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") | ||||
| endif() | ||||
|   | ||||
							
								
								
									
										65
									
								
								src/yuzu/compatdb.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/yuzu/compatdb.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <QButtonGroup> | ||||
| #include <QMessageBox> | ||||
| #include <QPushButton> | ||||
| #include "common/logging/log.h" | ||||
| #include "common/telemetry.h" | ||||
| #include "core/core.h" | ||||
| #include "core/telemetry_session.h" | ||||
| #include "ui_compatdb.h" | ||||
| #include "yuzu/compatdb.h" | ||||
|  | ||||
| CompatDB::CompatDB(QWidget* parent) | ||||
|     : QWizard(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), | ||||
|       ui{std::make_unique<Ui::CompatDB>()} { | ||||
|     ui->setupUi(this); | ||||
|     connect(ui->radioButton_Perfect, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(ui->radioButton_Great, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(ui->radioButton_Okay, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(ui->radioButton_Bad, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(ui->radioButton_IntroMenu, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(ui->radioButton_WontBoot, &QRadioButton::clicked, this, &CompatDB::EnableNext); | ||||
|     connect(button(NextButton), &QPushButton::clicked, this, &CompatDB::Submit); | ||||
| } | ||||
|  | ||||
| CompatDB::~CompatDB() = default; | ||||
|  | ||||
| enum class CompatDBPage { | ||||
|     Intro = 0, | ||||
|     Selection = 1, | ||||
|     Final = 2, | ||||
| }; | ||||
|  | ||||
| void CompatDB::Submit() { | ||||
|     QButtonGroup* compatibility = new QButtonGroup(this); | ||||
|     compatibility->addButton(ui->radioButton_Perfect, 0); | ||||
|     compatibility->addButton(ui->radioButton_Great, 1); | ||||
|     compatibility->addButton(ui->radioButton_Okay, 2); | ||||
|     compatibility->addButton(ui->radioButton_Bad, 3); | ||||
|     compatibility->addButton(ui->radioButton_IntroMenu, 4); | ||||
|     compatibility->addButton(ui->radioButton_WontBoot, 5); | ||||
|     switch ((static_cast<CompatDBPage>(currentId()))) { | ||||
|     case CompatDBPage::Selection: | ||||
|         if (compatibility->checkedId() == -1) { | ||||
|             button(NextButton)->setEnabled(false); | ||||
|         } | ||||
|         break; | ||||
|     case CompatDBPage::Final: | ||||
|         LOG_DEBUG(Frontend, "Compatibility Rating: {}", compatibility->checkedId()); | ||||
|         Core::Telemetry().AddField(Telemetry::FieldType::UserFeedback, "Compatibility", | ||||
|                                    compatibility->checkedId()); | ||||
|         // older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a | ||||
|         // workaround | ||||
|         button(QWizard::CancelButton)->setVisible(false); | ||||
|         break; | ||||
|     default: | ||||
|         LOG_ERROR(Frontend, "Unexpected page: {}", currentId()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void CompatDB::EnableNext() { | ||||
|     button(NextButton)->setEnabled(true); | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/yuzu/compatdb.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/yuzu/compatdb.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <memory> | ||||
| #include <QWizard> | ||||
|  | ||||
| namespace Ui { | ||||
| class CompatDB; | ||||
| } | ||||
|  | ||||
| class CompatDB : public QWizard { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit CompatDB(QWidget* parent = nullptr); | ||||
|     ~CompatDB(); | ||||
|  | ||||
| private: | ||||
|     std::unique_ptr<Ui::CompatDB> ui; | ||||
|  | ||||
|     void Submit(); | ||||
|     void EnableNext(); | ||||
| }; | ||||
							
								
								
									
										215
									
								
								src/yuzu/compatdb.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/yuzu/compatdb.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>CompatDB</class> | ||||
|  <widget class="QWizard" name="CompatDB"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>600</width> | ||||
|     <height>482</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="minimumSize"> | ||||
|    <size> | ||||
|     <width>500</width> | ||||
|     <height>410</height> | ||||
|    </size> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Report Compatibility</string> | ||||
|   </property> | ||||
|   <property name="options"> | ||||
|    <set>QWizard::DisabledBackButtonOnLastPage|QWizard::HelpButtonOnRight|QWizard::NoBackButtonOnStartPage</set> | ||||
|   </property> | ||||
|   <widget class="QWizardPage" name="wizard_Info"> | ||||
|    <property name="title"> | ||||
|     <string>Report Game Compatibility</string> | ||||
|    </property> | ||||
|    <attribute name="pageId"> | ||||
|     <string notr="true">0</string> | ||||
|    </attribute> | ||||
|    <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|     <item> | ||||
|      <widget class="QLabel" name="lbl_Spiel"> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|       <property name="openExternalLinks"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item> | ||||
|      <spacer name="verticalSpacer_2"> | ||||
|       <property name="orientation"> | ||||
|        <enum>Qt::Vertical</enum> | ||||
|       </property> | ||||
|       <property name="sizeHint" stdset="0"> | ||||
|        <size> | ||||
|         <width>20</width> | ||||
|         <height>0</height> | ||||
|        </size> | ||||
|       </property> | ||||
|      </spacer> | ||||
|     </item> | ||||
|    </layout> | ||||
|   </widget> | ||||
|   <widget class="QWizardPage" name="wizard_Report"> | ||||
|    <property name="title"> | ||||
|     <string>Report Game Compatibility</string> | ||||
|    </property> | ||||
|    <attribute name="pageId"> | ||||
|     <string notr="true">1</string> | ||||
|    </attribute> | ||||
|    <layout class="QFormLayout" name="formLayout"> | ||||
|     <item row="2" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_Perfect"> | ||||
|       <property name="text"> | ||||
|        <string>Perfect</string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="2" column="1"> | ||||
|      <widget class="QLabel" name="lbl_Perfect"> | ||||
|       <property name="text"> | ||||
|        <string><html><head/><body><p>Game functions flawlessly with no audio or graphical glitches.</p></body></html></string> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="4" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_Great"> | ||||
|       <property name="text"> | ||||
|        <string>Great </string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="4" column="1"> | ||||
|      <widget class="QLabel" name="lbl_Great"> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="5" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_Okay"> | ||||
|       <property name="text"> | ||||
|        <string>Okay</string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="5" column="1"> | ||||
|      <widget class="QLabel" name="lbl_Okay"> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="6" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_Bad"> | ||||
|       <property name="text"> | ||||
|        <string>Bad</string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="6" column="1"> | ||||
|      <widget class="QLabel" name="lbl_Bad"> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="7" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_IntroMenu"> | ||||
|       <property name="text"> | ||||
|        <string>Intro/Menu</string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="7" column="1"> | ||||
|      <widget class="QLabel" name="lbl_IntroMenu"> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="8" column="0"> | ||||
|      <widget class="QRadioButton" name="radioButton_WontBoot"> | ||||
|       <property name="text"> | ||||
|        <string>Won't Boot</string> | ||||
|       </property> | ||||
|       <property name="checkable"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|       <property name="checked"> | ||||
|        <bool>false</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="8" column="1"> | ||||
|      <widget class="QLabel" name="lbl_WontBoot"> | ||||
|       <property name="text"> | ||||
|        <string><html><head/><body><p>The game crashes when attempting to startup.</p></body></html></string> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="0" column="0" colspan="2"> | ||||
|      <widget class="QLabel" name="lbl_Independent"> | ||||
|       <property name="font"> | ||||
|        <font> | ||||
|         <pointsize>10</pointsize> | ||||
|        </font> | ||||
|       </property> | ||||
|       <property name="text"> | ||||
|        <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> | ||||
|       </property> | ||||
|       <property name="wordWrap"> | ||||
|        <bool>true</bool> | ||||
|       </property> | ||||
|      </widget> | ||||
|     </item> | ||||
|     <item row="1" column="0" colspan="2"> | ||||
|      <spacer name="verticalSpacer"> | ||||
|       <property name="orientation"> | ||||
|        <enum>Qt::Vertical</enum> | ||||
|       </property> | ||||
|       <property name="sizeHint" stdset="0"> | ||||
|        <size> | ||||
|         <width>20</width> | ||||
|         <height>0</height> | ||||
|        </size> | ||||
|       </property> | ||||
|      </spacer> | ||||
|     </item> | ||||
|    </layout> | ||||
|   </widget> | ||||
|   <widget class="QWizardPage" name="wizard_ThankYou"> | ||||
|    <property name="title"> | ||||
|     <string>Thank you for your submission!</string> | ||||
|    </property> | ||||
|    <attribute name="pageId"> | ||||
|     <string notr="true">2</string> | ||||
|    </attribute> | ||||
|   </widget> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
| @@ -136,8 +136,18 @@ void Config::ReadValues() { | ||||
|     Settings::values.gdbstub_port = qt_config->value("gdbstub_port", 24689).toInt(); | ||||
|     qt_config->endGroup(); | ||||
|  | ||||
|     qt_config->beginGroup("WebService"); | ||||
|     Settings::values.enable_telemetry = qt_config->value("enable_telemetry", true).toBool(); | ||||
|     Settings::values.web_api_url = | ||||
|         qt_config->value("web_api_url", "https://api.yuzu-emu.org").toString().toStdString(); | ||||
|     Settings::values.yuzu_username = qt_config->value("yuzu_username").toString().toStdString(); | ||||
|     Settings::values.yuzu_token = qt_config->value("yuzu_token").toString().toStdString(); | ||||
|     qt_config->endGroup(); | ||||
|  | ||||
|     qt_config->beginGroup("UI"); | ||||
|     UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); | ||||
|     UISettings::values.enable_discord_presence = | ||||
|         qt_config->value("enable_discord_presence", true).toBool(); | ||||
|  | ||||
|     qt_config->beginGroup("UIGameList"); | ||||
|     UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool(); | ||||
| @@ -261,8 +271,16 @@ void Config::SaveValues() { | ||||
|     qt_config->setValue("gdbstub_port", Settings::values.gdbstub_port); | ||||
|     qt_config->endGroup(); | ||||
|  | ||||
|     qt_config->beginGroup("WebService"); | ||||
|     qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry); | ||||
|     qt_config->setValue("web_api_url", QString::fromStdString(Settings::values.web_api_url)); | ||||
|     qt_config->setValue("yuzu_username", QString::fromStdString(Settings::values.yuzu_username)); | ||||
|     qt_config->setValue("yuzu_token", QString::fromStdString(Settings::values.yuzu_token)); | ||||
|     qt_config->endGroup(); | ||||
|  | ||||
|     qt_config->beginGroup("UI"); | ||||
|     qt_config->setValue("theme", UISettings::values.theme); | ||||
|     qt_config->setValue("enable_discord_presence", UISettings::values.enable_discord_presence); | ||||
|  | ||||
|     qt_config->beginGroup("UIGameList"); | ||||
|     qt_config->setValue("show_unknown", UISettings::values.show_unknown); | ||||
|   | ||||
| @@ -54,6 +54,11 @@ | ||||
|        <string>Debug</string> | ||||
|       </attribute> | ||||
|      </widget> | ||||
|       <widget class="ConfigureWeb" name="webTab"> | ||||
|         <attribute name="title"> | ||||
|           <string>Web</string> | ||||
|         </attribute> | ||||
|       </widget> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
| @@ -108,6 +113,12 @@ | ||||
|    <header>configuration/configure_graphics.h</header> | ||||
|    <container>1</container> | ||||
|   </customwidget> | ||||
|    <customwidget> | ||||
|      <class>ConfigureWeb</class> | ||||
|      <extends>QWidget</extends> | ||||
|      <header>configuration/configure_web.h</header> | ||||
|      <container>1</container> | ||||
|    </customwidget> | ||||
|  </customwidgets> | ||||
|  <resources/> | ||||
|  <connections> | ||||
|   | ||||
| @@ -27,5 +27,6 @@ void ConfigureDialog::applyConfiguration() { | ||||
|     ui->graphicsTab->applyConfiguration(); | ||||
|     ui->audioTab->applyConfiguration(); | ||||
|     ui->debugTab->applyConfiguration(); | ||||
|     ui->webTab->applyConfiguration(); | ||||
|     Settings::Apply(); | ||||
| } | ||||
|   | ||||
							
								
								
									
										119
									
								
								src/yuzu/configuration/configure_web.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/yuzu/configuration/configure_web.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <QIcon> | ||||
| #include <QMessageBox> | ||||
| #include <QtConcurrent/QtConcurrentRun> | ||||
| #include "core/settings.h" | ||||
| #include "core/telemetry_session.h" | ||||
| #include "ui_configure_web.h" | ||||
| #include "yuzu/configuration/configure_web.h" | ||||
| #include "yuzu/ui_settings.h" | ||||
|  | ||||
| ConfigureWeb::ConfigureWeb(QWidget* parent) | ||||
|     : QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { | ||||
|     ui->setupUi(this); | ||||
|     connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this, | ||||
|             &ConfigureWeb::RefreshTelemetryID); | ||||
|     connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin); | ||||
|     connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified); | ||||
|  | ||||
| #ifndef USE_DISCORD_PRESENCE | ||||
|     ui->discord_group->setVisible(false); | ||||
| #endif | ||||
|     this->setConfiguration(); | ||||
| } | ||||
|  | ||||
| ConfigureWeb::~ConfigureWeb() = default; | ||||
|  | ||||
| void ConfigureWeb::setConfiguration() { | ||||
|     ui->web_credentials_disclaimer->setWordWrap(true); | ||||
|     ui->telemetry_learn_more->setOpenExternalLinks(true); | ||||
|     ui->telemetry_learn_more->setText( | ||||
|         tr("<a href='https://yuzu-emu.org/help/features/telemetry/'><span style=\"text-decoration: " | ||||
|            "underline; color:#039be5;\">Learn more</span></a>")); | ||||
|  | ||||
|     ui->web_signup_link->setOpenExternalLinks(true); | ||||
|     ui->web_signup_link->setText( | ||||
|         tr("<a href='https://profile.yuzu-emu.org/'><span style=\"text-decoration: underline; " | ||||
|            "color:#039be5;\">Sign up</span></a>")); | ||||
|     ui->web_token_info_link->setOpenExternalLinks(true); | ||||
|     ui->web_token_info_link->setText( | ||||
|         tr("<a href='https://yuzu-emu.org/wiki/yuzu-web-service/'><span style=\"text-decoration: " | ||||
|            "underline; color:#039be5;\">What is my token?</span></a>")); | ||||
|  | ||||
|     ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); | ||||
|     ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username)); | ||||
|     ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token)); | ||||
|     // Connect after setting the values, to avoid calling OnLoginChanged now | ||||
|     connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); | ||||
|     connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); | ||||
|     ui->label_telemetry_id->setText( | ||||
|         tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); | ||||
|     user_verified = true; | ||||
|  | ||||
|     ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence); | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::applyConfiguration() { | ||||
|     Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); | ||||
|     UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); | ||||
|     if (user_verified) { | ||||
|         Settings::values.yuzu_username = ui->edit_username->text().toStdString(); | ||||
|         Settings::values.yuzu_token = ui->edit_token->text().toStdString(); | ||||
|     } else { | ||||
|         QMessageBox::warning(this, tr("Username and token not verified"), | ||||
|                              tr("Username and token were not verified. The changes to your " | ||||
|                                 "username and/or token have not been saved.")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::RefreshTelemetryID() { | ||||
|     const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; | ||||
|     ui->label_telemetry_id->setText( | ||||
|         tr("Telemetry ID: 0x%1").arg(QString::number(new_telemetry_id, 16).toUpper())); | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::OnLoginChanged() { | ||||
|     if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { | ||||
|         user_verified = true; | ||||
|         ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|         ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|     } else { | ||||
|         user_verified = false; | ||||
|         ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||||
|         ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::VerifyLogin() { | ||||
|     ui->button_verify_login->setDisabled(true); | ||||
|     ui->button_verify_login->setText(tr("Verifying")); | ||||
|     verify_watcher.setFuture( | ||||
|         QtConcurrent::run([this, username = ui->edit_username->text().toStdString(), | ||||
|                            token = ui->edit_token->text().toStdString()]() { | ||||
|             return Core::VerifyLogin(username, token); | ||||
|         })); | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::OnLoginVerified() { | ||||
|     ui->button_verify_login->setEnabled(true); | ||||
|     ui->button_verify_login->setText(tr("Verify")); | ||||
|     if (verify_watcher.result()) { | ||||
|         user_verified = true; | ||||
|         ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|         ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); | ||||
|     } else { | ||||
|         ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||||
|         ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); | ||||
|         QMessageBox::critical( | ||||
|             this, tr("Verification failed"), | ||||
|             tr("Verification failed. Check that you have entered your username and token " | ||||
|                "correctly, and that your internet connection is working.")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void ConfigureWeb::retranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/yuzu/configuration/configure_web.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/yuzu/configuration/configure_web.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| // Copyright 2017 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <memory> | ||||
| #include <QFutureWatcher> | ||||
| #include <QWidget> | ||||
|  | ||||
| namespace Ui { | ||||
| class ConfigureWeb; | ||||
| } | ||||
|  | ||||
| class ConfigureWeb : public QWidget { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit ConfigureWeb(QWidget* parent = nullptr); | ||||
|     ~ConfigureWeb(); | ||||
|  | ||||
|     void applyConfiguration(); | ||||
|     void retranslateUi(); | ||||
|  | ||||
| public slots: | ||||
|     void RefreshTelemetryID(); | ||||
|     void OnLoginChanged(); | ||||
|     void VerifyLogin(); | ||||
|     void OnLoginVerified(); | ||||
|  | ||||
| private: | ||||
|     void setConfiguration(); | ||||
|  | ||||
|     bool user_verified = true; | ||||
|     QFutureWatcher<bool> verify_watcher; | ||||
|  | ||||
|     std::unique_ptr<Ui::ConfigureWeb> ui; | ||||
| }; | ||||
							
								
								
									
										206
									
								
								src/yuzu/configuration/configure_web.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/yuzu/configuration/configure_web.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ConfigureWeb</class> | ||||
|  <widget class="QWidget" name="ConfigureWeb"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>926</width> | ||||
|     <height>561</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Form</string> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|    <item> | ||||
|     <layout class="QVBoxLayout" name="verticalLayout_3"> | ||||
|      <item> | ||||
|       <widget class="QGroupBox" name="groupBoxWebConfig"> | ||||
|        <property name="title"> | ||||
|         <string>yuzu Web Service</string> | ||||
|        </property> | ||||
|        <layout class="QVBoxLayout" name="verticalLayoutYuzuWebService"> | ||||
|         <item> | ||||
|          <widget class="QLabel" name="web_credentials_disclaimer"> | ||||
|           <property name="text"> | ||||
|            <string>By providing your username and token, you agree to allow yuzu to collect additional usage data, which may include user identifying information.</string> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item> | ||||
|          <layout class="QGridLayout" name="gridLayoutYuzuUsername"> | ||||
|           <item row="2" column="3"> | ||||
|            <widget class="QPushButton" name="button_verify_login"> | ||||
|             <property name="sizePolicy"> | ||||
|              <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> | ||||
|               <horstretch>0</horstretch> | ||||
|               <verstretch>0</verstretch> | ||||
|              </sizepolicy> | ||||
|             </property> | ||||
|             <property name="layoutDirection"> | ||||
|              <enum>Qt::RightToLeft</enum> | ||||
|             </property> | ||||
|             <property name="text"> | ||||
|              <string>Verify</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="2" column="0"> | ||||
|            <widget class="QLabel" name="web_signup_link"> | ||||
|             <property name="text"> | ||||
|              <string>Sign up</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="1" colspan="3"> | ||||
|            <widget class="QLineEdit" name="edit_username"> | ||||
|             <property name="maxLength"> | ||||
|              <number>36</number> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="1" column="0"> | ||||
|            <widget class="QLabel" name="label_token"> | ||||
|             <property name="text"> | ||||
|              <string>Token: </string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="1" column="4"> | ||||
|            <widget class="QLabel" name="label_token_verified"> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="0"> | ||||
|            <widget class="QLabel" name="label_username"> | ||||
|             <property name="text"> | ||||
|              <string>Username: </string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="4"> | ||||
|            <widget class="QLabel" name="label_username_verified"> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="1" column="1" colspan="3"> | ||||
|            <widget class="QLineEdit" name="edit_token"> | ||||
|             <property name="maxLength"> | ||||
|              <number>36</number> | ||||
|             </property> | ||||
|             <property name="echoMode"> | ||||
|              <enum>QLineEdit::Password</enum> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="2" column="1"> | ||||
|            <widget class="QLabel" name="web_token_info_link"> | ||||
|             <property name="text"> | ||||
|              <string>What is my token?</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="2" column="2"> | ||||
|            <spacer name="horizontalSpacer"> | ||||
|             <property name="orientation"> | ||||
|              <enum>Qt::Horizontal</enum> | ||||
|             </property> | ||||
|             <property name="sizeHint" stdset="0"> | ||||
|              <size> | ||||
|               <width>40</width> | ||||
|               <height>20</height> | ||||
|              </size> | ||||
|             </property> | ||||
|            </spacer> | ||||
|           </item> | ||||
|          </layout> | ||||
|         </item> | ||||
|        </layout> | ||||
|       </widget> | ||||
|      </item> | ||||
|      <item> | ||||
|       <widget class="QGroupBox" name="groupBox"> | ||||
|        <property name="title"> | ||||
|         <string>Telemetry</string> | ||||
|        </property> | ||||
|        <layout class="QVBoxLayout" name="verticalLayout_2"> | ||||
|         <item> | ||||
|          <widget class="QCheckBox" name="toggle_telemetry"> | ||||
|           <property name="text"> | ||||
|            <string>Share anonymous usage data with the yuzu team</string> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item> | ||||
|          <widget class="QLabel" name="telemetry_learn_more"> | ||||
|           <property name="text"> | ||||
|            <string>Learn more</string> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item> | ||||
|          <layout class="QGridLayout" name="gridLayoutTelemetryId"> | ||||
|           <item row="0" column="0"> | ||||
|            <widget class="QLabel" name="label_telemetry_id"> | ||||
|             <property name="text"> | ||||
|              <string>Telemetry ID:</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|           <item row="0" column="1"> | ||||
|            <widget class="QPushButton" name="button_regenerate_telemetry_id"> | ||||
|             <property name="sizePolicy"> | ||||
|              <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> | ||||
|               <horstretch>0</horstretch> | ||||
|               <verstretch>0</verstretch> | ||||
|              </sizepolicy> | ||||
|             </property> | ||||
|             <property name="layoutDirection"> | ||||
|              <enum>Qt::RightToLeft</enum> | ||||
|             </property> | ||||
|             <property name="text"> | ||||
|              <string>Regenerate</string> | ||||
|             </property> | ||||
|            </widget> | ||||
|           </item> | ||||
|          </layout> | ||||
|         </item> | ||||
|        </layout> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|      <widget class="QGroupBox" name="discord_group"> | ||||
|       <property name="title"> | ||||
|        <string>Discord Presence</string> | ||||
|       </property> | ||||
|       <layout class="QVBoxLayout" name="verticalLayout_21"> | ||||
|        <item> | ||||
|         <widget class="QCheckBox" name="toggle_discordrpc"> | ||||
|          <property name="text"> | ||||
|           <string>Show Current Game in your Discord Status</string> | ||||
|          </property> | ||||
|         </widget> | ||||
|        </item> | ||||
|       </layout> | ||||
|      </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <spacer name="verticalSpacer"> | ||||
|      <property name="orientation"> | ||||
|       <enum>Qt::Vertical</enum> | ||||
|      </property> | ||||
|      <property name="sizeHint" stdset="0"> | ||||
|       <size> | ||||
|        <width>20</width> | ||||
|        <height>40</height> | ||||
|       </size> | ||||
|      </property> | ||||
|     </spacer> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
							
								
								
									
										25
									
								
								src/yuzu/discord.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/yuzu/discord.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // Copyright 2018 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| namespace DiscordRPC { | ||||
|  | ||||
| class DiscordInterface { | ||||
| public: | ||||
|     virtual ~DiscordInterface() = default; | ||||
|  | ||||
|     virtual void Pause() = 0; | ||||
|     virtual void Update() = 0; | ||||
| }; | ||||
|  | ||||
| class NullImpl : public DiscordInterface { | ||||
| public: | ||||
|     ~NullImpl() = default; | ||||
|  | ||||
|     void Pause() override {} | ||||
|     void Update() override {} | ||||
| }; | ||||
|  | ||||
| } // namespace DiscordRPC | ||||
							
								
								
									
										52
									
								
								src/yuzu/discord_impl.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/yuzu/discord_impl.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // Copyright 2018 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <chrono> | ||||
| #include <string> | ||||
| #include <discord_rpc.h> | ||||
| #include "common/common_types.h" | ||||
| #include "core/core.h" | ||||
| #include "core/loader/loader.h" | ||||
| #include "yuzu/discord_impl.h" | ||||
| #include "yuzu/ui_settings.h" | ||||
|  | ||||
| namespace DiscordRPC { | ||||
|  | ||||
| DiscordImpl::DiscordImpl() { | ||||
|     DiscordEventHandlers handlers{}; | ||||
|  | ||||
|     // The number is the client ID for yuzu, it's used for images and the | ||||
|     // application name | ||||
|     Discord_Initialize("471872241299226636", &handlers, 1, nullptr); | ||||
| } | ||||
|  | ||||
| DiscordImpl::~DiscordImpl() { | ||||
|     Discord_ClearPresence(); | ||||
|     Discord_Shutdown(); | ||||
| } | ||||
|  | ||||
| void DiscordImpl::Pause() { | ||||
|     Discord_ClearPresence(); | ||||
| } | ||||
|  | ||||
| void DiscordImpl::Update() { | ||||
|     s64 start_time = std::chrono::duration_cast<std::chrono::seconds>( | ||||
|                          std::chrono::system_clock::now().time_since_epoch()) | ||||
|                          .count(); | ||||
|     std::string title; | ||||
|     if (Core::System::GetInstance().IsPoweredOn()) | ||||
|         Core::System::GetInstance().GetAppLoader().ReadTitle(title); | ||||
|     DiscordRichPresence presence{}; | ||||
|     presence.largeImageKey = "yuzu_logo"; | ||||
|     presence.largeImageText = "yuzu is an emulator for the Nintendo Switch"; | ||||
|     if (Core::System::GetInstance().IsPoweredOn()) { | ||||
|         presence.state = title.c_str(); | ||||
|         presence.details = "Currently in game"; | ||||
|     } else { | ||||
|         presence.details = "Not in game"; | ||||
|     } | ||||
|     presence.startTimestamp = start_time; | ||||
|     Discord_UpdatePresence(&presence); | ||||
| } | ||||
| } // namespace DiscordRPC | ||||
							
								
								
									
										20
									
								
								src/yuzu/discord_impl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/yuzu/discord_impl.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| // Copyright 2018 Citra Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "yuzu/discord.h" | ||||
|  | ||||
| namespace DiscordRPC { | ||||
|  | ||||
| class DiscordImpl : public DiscordInterface { | ||||
| public: | ||||
|     DiscordImpl(); | ||||
|     ~DiscordImpl() override; | ||||
|  | ||||
|     void Pause() override; | ||||
|     void Update() override; | ||||
| }; | ||||
|  | ||||
| } // namespace DiscordRPC | ||||
| @@ -35,6 +35,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | ||||
| #include <QtWidgets> | ||||
| #include <fmt/format.h> | ||||
| #include "common/common_paths.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/logging/backend.h" | ||||
| #include "common/logging/filter.h" | ||||
| @@ -65,6 +66,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | ||||
| #include "video_core/debug_utils/debug_utils.h" | ||||
| #include "yuzu/about_dialog.h" | ||||
| #include "yuzu/bootmanager.h" | ||||
| #include "yuzu/compatdb.h" | ||||
| #include "yuzu/compatibility_list.h" | ||||
| #include "yuzu/configuration/config.h" | ||||
| #include "yuzu/configuration/configure_dialog.h" | ||||
| @@ -73,12 +75,17 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | ||||
| #include "yuzu/debugger/graphics/graphics_surface.h" | ||||
| #include "yuzu/debugger/profiler.h" | ||||
| #include "yuzu/debugger/wait_tree.h" | ||||
| #include "yuzu/discord.h" | ||||
| #include "yuzu/game_list.h" | ||||
| #include "yuzu/game_list_p.h" | ||||
| #include "yuzu/hotkeys.h" | ||||
| #include "yuzu/main.h" | ||||
| #include "yuzu/ui_settings.h" | ||||
|  | ||||
| #ifdef USE_DISCORD_PRESENCE | ||||
| #include "yuzu/discord_impl.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef QT_STATICPLUGIN | ||||
| Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); | ||||
| #endif | ||||
| @@ -102,23 +109,22 @@ enum class CalloutFlag : uint32_t { | ||||
|     DRDDeprecation = 0x2, | ||||
| }; | ||||
|  | ||||
| static void ShowCalloutMessage(const QString& message, CalloutFlag flag) { | ||||
|     if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) { | ||||
| void GMainWindow::ShowTelemetryCallout() { | ||||
|     if (UISettings::values.callout_flags & static_cast<uint32_t>(CalloutFlag::Telemetry)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     UISettings::values.callout_flags |= static_cast<uint32_t>(flag); | ||||
|  | ||||
|     QMessageBox msg; | ||||
|     msg.setText(message); | ||||
|     msg.setStandardButtons(QMessageBox::Ok); | ||||
|     msg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); | ||||
|     msg.setStyleSheet("QLabel{min-width: 900px;}"); | ||||
|     msg.exec(); | ||||
|     UISettings::values.callout_flags |= static_cast<uint32_t>(CalloutFlag::Telemetry); | ||||
|     const QString telemetry_message = | ||||
|         tr("<a href='https://yuzu-emu.org/help/features/telemetry/'>Anonymous " | ||||
|            "data is collected</a> to help improve yuzu. " | ||||
|            "<br/><br/>Would you like to share your usage data with us?"); | ||||
|     if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) { | ||||
|         Settings::values.enable_telemetry = false; | ||||
|         Settings::Apply(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GMainWindow::ShowCallouts() {} | ||||
|  | ||||
| const int GMainWindow::max_recent_files_item; | ||||
|  | ||||
| static void InitializeLogging() { | ||||
| @@ -145,6 +151,9 @@ GMainWindow::GMainWindow() | ||||
|     default_theme_paths = QIcon::themeSearchPaths(); | ||||
|     UpdateUITheme(); | ||||
|  | ||||
|     SetDiscordEnabled(UISettings::values.enable_discord_presence); | ||||
|     discord_rpc->Update(); | ||||
|  | ||||
|     InitializeWidgets(); | ||||
|     InitializeDebugWidgets(); | ||||
|     InitializeRecentFileMenuActions(); | ||||
| @@ -168,7 +177,7 @@ GMainWindow::GMainWindow() | ||||
|     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||||
|  | ||||
|     // Show one-time "callout" messages to the user | ||||
|     ShowCallouts(); | ||||
|     ShowTelemetryCallout(); | ||||
|  | ||||
|     QStringList args = QApplication::arguments(); | ||||
|     if (args.length() >= 2) { | ||||
| @@ -183,6 +192,9 @@ GMainWindow::~GMainWindow() { | ||||
| } | ||||
|  | ||||
| void GMainWindow::InitializeWidgets() { | ||||
| #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING | ||||
|     ui.action_Report_Compatibility->setVisible(true); | ||||
| #endif | ||||
|     render_window = new GRenderWindow(this, emu_thread.get()); | ||||
|     render_window->hide(); | ||||
|  | ||||
| @@ -411,6 +423,8 @@ void GMainWindow::ConnectMenuEvents() { | ||||
|     connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame); | ||||
|     connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame); | ||||
|     connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame); | ||||
|     connect(ui.action_Report_Compatibility, &QAction::triggered, this, | ||||
|             &GMainWindow::OnMenuReportCompatibility); | ||||
|     connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); | ||||
|     connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); | ||||
|  | ||||
| @@ -647,6 +661,7 @@ void GMainWindow::BootGame(const QString& filename) { | ||||
| } | ||||
|  | ||||
| void GMainWindow::ShutdownGame() { | ||||
|     discord_rpc->Pause(); | ||||
|     emu_thread->RequestStop(); | ||||
|  | ||||
|     emit EmulationStopping(); | ||||
| @@ -655,6 +670,8 @@ void GMainWindow::ShutdownGame() { | ||||
|     emu_thread->wait(); | ||||
|     emu_thread = nullptr; | ||||
|  | ||||
|     discord_rpc->Update(); | ||||
|  | ||||
|     // The emulation is stopped, so closing the window or not does not matter anymore | ||||
|     disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); | ||||
|  | ||||
| @@ -664,6 +681,7 @@ void GMainWindow::ShutdownGame() { | ||||
|     ui.action_Pause->setEnabled(false); | ||||
|     ui.action_Stop->setEnabled(false); | ||||
|     ui.action_Restart->setEnabled(false); | ||||
|     ui.action_Report_Compatibility->setEnabled(false); | ||||
|     render_window->hide(); | ||||
|     game_list->show(); | ||||
|     game_list->setFilterFocus(); | ||||
| @@ -1147,6 +1165,9 @@ void GMainWindow::OnStartGame() { | ||||
|     ui.action_Pause->setEnabled(true); | ||||
|     ui.action_Stop->setEnabled(true); | ||||
|     ui.action_Restart->setEnabled(true); | ||||
|     ui.action_Report_Compatibility->setEnabled(true); | ||||
|  | ||||
|     discord_rpc->Update(); | ||||
| } | ||||
|  | ||||
| void GMainWindow::OnPauseGame() { | ||||
| @@ -1161,6 +1182,20 @@ void GMainWindow::OnStopGame() { | ||||
|     ShutdownGame(); | ||||
| } | ||||
|  | ||||
| void GMainWindow::OnMenuReportCompatibility() { | ||||
|     if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) { | ||||
|         CompatDB compatdb{this}; | ||||
|         compatdb.exec(); | ||||
|     } else { | ||||
|         QMessageBox::critical( | ||||
|             this, tr("Missing yuzu Account"), | ||||
|             tr("In order to submit a game compatibility test case, you must link your yuzu " | ||||
|                "account.<br><br/>To link your yuzu account, go to Emulation > Configuration " | ||||
|                "> " | ||||
|                "Web.")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void GMainWindow::ToggleFullscreen() { | ||||
|     if (!emulation_running) { | ||||
|         return; | ||||
| @@ -1224,11 +1259,14 @@ void GMainWindow::ToggleWindowMode() { | ||||
| void GMainWindow::OnConfigure() { | ||||
|     ConfigureDialog configureDialog(this, hotkey_registry); | ||||
|     auto old_theme = UISettings::values.theme; | ||||
|     const bool old_discord_presence = UISettings::values.enable_discord_presence; | ||||
|     auto result = configureDialog.exec(); | ||||
|     if (result == QDialog::Accepted) { | ||||
|         configureDialog.applyConfiguration(); | ||||
|         if (UISettings::values.theme != old_theme) | ||||
|             UpdateUITheme(); | ||||
|         if (UISettings::values.enable_discord_presence != old_discord_presence) | ||||
|             SetDiscordEnabled(UISettings::values.enable_discord_presence); | ||||
|         game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); | ||||
|         config->Save(); | ||||
|     } | ||||
| @@ -1443,11 +1481,25 @@ void GMainWindow::UpdateUITheme() { | ||||
|     emit UpdateThemedIcons(); | ||||
| } | ||||
|  | ||||
| void GMainWindow::SetDiscordEnabled(bool state) { | ||||
| #ifdef USE_DISCORD_PRESENCE | ||||
|     if (state) { | ||||
|         discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>(); | ||||
|     } else { | ||||
|         discord_rpc = std::make_unique<DiscordRPC::NullImpl>(); | ||||
|     } | ||||
| #else | ||||
|     discord_rpc = std::make_unique<DiscordRPC::NullImpl>(); | ||||
| #endif | ||||
|     discord_rpc->Update(); | ||||
| } | ||||
|  | ||||
| #ifdef main | ||||
| #undef main | ||||
| #endif | ||||
|  | ||||
| int main(int argc, char* argv[]) { | ||||
|     Common::DetachedTasks detached_tasks; | ||||
|     MicroProfileOnThreadCreate("Frontend"); | ||||
|     SCOPE_EXIT({ MicroProfileShutdown(); }); | ||||
|  | ||||
| @@ -1465,5 +1517,7 @@ int main(int argc, char* argv[]) { | ||||
|     GMainWindow main_window; | ||||
|     // After settings have been loaded by GMainWindow, apply the filter | ||||
|     main_window.show(); | ||||
|     return app.exec(); | ||||
|     int result = app.exec(); | ||||
|     detached_tasks.WaitForAllTasks(); | ||||
|     return result; | ||||
| } | ||||
|   | ||||
| @@ -41,6 +41,10 @@ enum class EmulatedDirectoryTarget { | ||||
|     SDMC, | ||||
| }; | ||||
|  | ||||
| namespace DiscordRPC { | ||||
| class DiscordInterface; | ||||
| } | ||||
|  | ||||
| class GMainWindow : public QMainWindow { | ||||
|     Q_OBJECT | ||||
|  | ||||
| @@ -61,6 +65,8 @@ public: | ||||
|     GMainWindow(); | ||||
|     ~GMainWindow() override; | ||||
|  | ||||
|     std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; | ||||
|  | ||||
| signals: | ||||
|  | ||||
|     /** | ||||
| @@ -99,7 +105,8 @@ private: | ||||
|     void BootGame(const QString& filename); | ||||
|     void ShutdownGame(); | ||||
|  | ||||
|     void ShowCallouts(); | ||||
|     void ShowTelemetryCallout(); | ||||
|     void SetDiscordEnabled(bool state); | ||||
|  | ||||
|     /** | ||||
|      * Stores the filename in the recently loaded files list. | ||||
| @@ -135,6 +142,7 @@ private slots: | ||||
|     void OnStartGame(); | ||||
|     void OnPauseGame(); | ||||
|     void OnStopGame(); | ||||
|     void OnMenuReportCompatibility(); | ||||
|     /// Called whenever a user selects a game in the game list widget. | ||||
|     void OnGameListLoadFile(QString game_path); | ||||
|     void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); | ||||
|   | ||||
| @@ -45,7 +45,7 @@ | ||||
|      <x>0</x> | ||||
|      <y>0</y> | ||||
|      <width>1081</width> | ||||
|      <height>19</height> | ||||
|      <height>21</height> | ||||
|     </rect> | ||||
|    </property> | ||||
|    <widget class="QMenu" name="menu_File"> | ||||
| @@ -101,6 +101,8 @@ | ||||
|     <property name="title"> | ||||
|      <string>&Help</string> | ||||
|     </property> | ||||
|     <addaction name="action_Report_Compatibility"/> | ||||
|     <addaction name="separator"/> | ||||
|     <addaction name="action_About"/> | ||||
|    </widget> | ||||
|    <addaction name="menu_File"/> | ||||
| @@ -239,6 +241,18 @@ | ||||
|        <string>Restart</string> | ||||
|      </property> | ||||
|    </action> | ||||
|    <action name="action_Report_Compatibility"> | ||||
|      <property name="enabled"> | ||||
|        <bool>false</bool> | ||||
|      </property> | ||||
|      <property name="text"> | ||||
|        <string>Report Compatibility</string> | ||||
|      </property> | ||||
|      <property name="visible"> | ||||
|        <bool>false</bool> | ||||
|      </property> | ||||
|    </action> | ||||
|   </widget> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
|   | ||||
| @@ -38,6 +38,9 @@ struct Values { | ||||
|     bool confirm_before_closing; | ||||
|     bool first_start; | ||||
|  | ||||
|     // Discord RPC | ||||
|     bool enable_discord_presence; | ||||
|  | ||||
|     QString roms_path; | ||||
|     QString symbols_path; | ||||
|     QString gamedir; | ||||
|   | ||||
| @@ -138,6 +138,14 @@ void Config::ReadValues() { | ||||
|     Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false); | ||||
|     Settings::values.gdbstub_port = | ||||
|         static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689)); | ||||
|  | ||||
|     // Web Service | ||||
|     Settings::values.enable_telemetry = | ||||
|         sdl2_config->GetBoolean("WebService", "enable_telemetry", true); | ||||
|     Settings::values.web_api_url = | ||||
|         sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org"); | ||||
|     Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", ""); | ||||
|     Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", ""); | ||||
| } | ||||
|  | ||||
| void Config::Reload() { | ||||
|   | ||||
| @@ -202,12 +202,10 @@ gdbstub_port=24689 | ||||
| # Whether or not to enable telemetry | ||||
| # 0: No, 1 (default): Yes | ||||
| enable_telemetry = | ||||
| # Endpoint URL for submitting telemetry data | ||||
| telemetry_endpoint_url = | ||||
| # Endpoint URL to verify the username and token | ||||
| verify_endpoint_url = | ||||
| # URL for Web API | ||||
| web_api_url = https://api.yuzu-emu.org | ||||
| # Username and token for yuzu Web Service | ||||
| # See https://services.citra-emu.org/ for more info | ||||
| # See https://profile.yuzu-emu.org/ for more info | ||||
| yuzu_username = | ||||
| yuzu_token = | ||||
| )"; | ||||
|   | ||||
| @@ -10,6 +10,7 @@ | ||||
| #include <fmt/ostream.h> | ||||
|  | ||||
| #include "common/common_paths.h" | ||||
| #include "common/detached_tasks.h" | ||||
| #include "common/file_util.h" | ||||
| #include "common/logging/backend.h" | ||||
| #include "common/logging/filter.h" | ||||
| @@ -78,6 +79,7 @@ static void InitializeLogging() { | ||||
|  | ||||
| /// Application entry point | ||||
| int main(int argc, char** argv) { | ||||
|     Common::DetachedTasks detached_tasks; | ||||
|     Config config; | ||||
|  | ||||
|     int option_index = 0; | ||||
| @@ -213,5 +215,6 @@ int main(int argc, char** argv) { | ||||
|         system.RunLoop(); | ||||
|     } | ||||
|  | ||||
|     detached_tasks.WaitForAllTasks(); | ||||
|     return 0; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user