diff --git a/.gitmodules b/.gitmodules index 4d4ad583f9..4ae9400cff 100644 --- a/.gitmodules +++ b/.gitmodules @@ -51,3 +51,6 @@ [submodule "Externals/gtest"] path = Externals/gtest url = https://github.com/google/googletest.git +[submodule "Externals/rcheevos/rcheevos"] + path = Externals/rcheevos/rcheevos + url = https://github.com/RetroAchievements/rcheevos.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4fd3972ad2..ae86d33b42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,6 +72,7 @@ option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence, show the current gam option(USE_MGBA "Enables GBA controllers emulation using libmgba" ON) option(ENABLE_AUTOUPDATE "Enables support for automatic updates" ON) option(STEAM "Creates a build for Steam" OFF) +option(USE_RETRO_ACHIEVEMENTS "Enables integration with retroachievements.org" ON) # Maintainers: if you consider blanket disabling this for your users, please # consider the following points: @@ -975,6 +976,10 @@ add_subdirectory(Externals/rangeset) add_subdirectory(Externals/FatFs) +if (USE_RETRO_ACHIEVEMENTS) + add_subdirectory(Externals/rcheevos) +endif() + ######################################## # Pre-build events: Define configuration variables and write SCM info header # diff --git a/Externals/rcheevos/CMakeLists.txt b/Externals/rcheevos/CMakeLists.txt new file mode 100644 index 0000000000..0fea1f9873 --- /dev/null +++ b/Externals/rcheevos/CMakeLists.txt @@ -0,0 +1,49 @@ +add_library(rcheevos + rcheevos/include/rc_api_editor.h + rcheevos/include/rc_api_info.h + rcheevos/include/rc_api_request.h + rcheevos/include/rc_api_runtime.h + rcheevos/include/rc_api_user.h + rcheevos/include/rc_consoles.h + rcheevos/include/rc_error.h + rcheevos/include/rc_hash.h + rcheevos/include/rcheevos.h + rcheevos/include/rc_runtime.h + rcheevos/include/rc_runtime_types.h + rcheevos/include/rc_url.h + rcheevos/src/rapi/rc_api_common.c + rcheevos/src/rapi/rc_api_common.h + rcheevos/src/rapi/rc_api_editor.c + rcheevos/src/rapi/rc_api_info.c + rcheevos/src/rapi/rc_api_runtime.c + rcheevos/src/rapi/rc_api_user.c + rcheevos/src/rcheevos/alloc.c + rcheevos/src/rcheevos/compat.c + rcheevos/src/rcheevos/condition.c + rcheevos/src/rcheevos/condset.c + rcheevos/src/rcheevos/consoleinfo.c + rcheevos/src/rcheevos/format.c + rcheevos/src/rcheevos/lboard.c + rcheevos/src/rcheevos/memref.c + rcheevos/src/rcheevos/operand.c + rcheevos/src/rcheevos/rc_compat.h + rcheevos/src/rcheevos/rc_internal.h + rcheevos/src/rcheevos/rc_validate.c + rcheevos/src/rcheevos/rc_validate.h + rcheevos/src/rcheevos/richpresence.c + rcheevos/src/rcheevos/runtime.c + rcheevos/src/rcheevos/runtime_progress.c + rcheevos/src/rcheevos/trigger.c + rcheevos/src/rcheevos/value.c + rcheevos/src/rhash/hash.c + rcheevos/src/rhash/md5.c + rcheevos/src/rhash/md5.h + rcheevos/src/rurl/url.c +) + +target_include_directories(rcheevos PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include") +target_include_directories(rcheevos INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_definitions(rcheevos PRIVATE "RC_DISABLE_LUA=1" "RCHEEVOS_URL_SSL") +if(CMAKE_SYSTEM_NAME MATCHES "Windows") + target_compile_definitions(rcheevos PRIVATE "_CRT_SECURE_NO_WARNINGS") +endif() diff --git a/Externals/rcheevos/exports.props b/Externals/rcheevos/exports.props new file mode 100644 index 0000000000..d5e26ded5e --- /dev/null +++ b/Externals/rcheevos/exports.props @@ -0,0 +1,13 @@ + + + + + $(ExternalsDir)rcheevos;%(AdditionalIncludeDirectories) + + + + + {CC99A910-3752-4465-95AA-7DC240D92A99} + + + diff --git a/Externals/rcheevos/rcheevos b/Externals/rcheevos/rcheevos new file mode 160000 index 0000000000..c5304a61bc --- /dev/null +++ b/Externals/rcheevos/rcheevos @@ -0,0 +1 @@ +Subproject commit c5304a61bcf256ae80fcd1c8f64ad9646aaea757 diff --git a/Externals/rcheevos/rcheevos.vcxproj b/Externals/rcheevos/rcheevos.vcxproj new file mode 100644 index 0000000000..cde8fd6ecd --- /dev/null +++ b/Externals/rcheevos/rcheevos.vcxproj @@ -0,0 +1,71 @@ + + + + + + {CC99A910-3752-4465-95AA-7DC240D92A99} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RC_DISABLE_LUA;RCHEEVOS_URL_SSL;%(PreprocessorDefinitions) + $(ProjectDir)rcheevos\include;%(AdditionalIncludeDirectories) + + + + + + \ No newline at end of file diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h index 047f5dc967..b22033a5cd 100644 --- a/Source/Core/Common/CommonPaths.h +++ b/Source/Core/Common/CommonPaths.h @@ -112,6 +112,7 @@ #define LOGGER_CONFIG "Logger.ini" #define DUALSHOCKUDPCLIENT_CONFIG "DSUClient.ini" #define FREELOOK_CONFIG "FreeLook.ini" +#define RETROACHIEVEMENTS_CONFIG "RetroAchievements.ini" // Files in the directory returned by GetUserPath(D_LOGS_IDX) #define MAIN_LOG "dolphin.log" diff --git a/Source/Core/Common/Config/Config.cpp b/Source/Core/Common/Config/Config.cpp index 4a55ab5b7b..ff3a8c6783 100644 --- a/Source/Core/Common/Config/Config.cpp +++ b/Source/Core/Common/Config/Config.cpp @@ -160,7 +160,8 @@ static const std::map system_to_name = { {System::DualShockUDPClient, "DualShockUDPClient"}, {System::FreeLook, "FreeLook"}, {System::Session, "Session"}, - {System::GameSettingsOnly, "GameSettingsOnly"}}; + {System::GameSettingsOnly, "GameSettingsOnly"}, + {System::Achievements, "Achievements"}}; const std::string& GetSystemName(System system) { diff --git a/Source/Core/Common/Config/Enums.h b/Source/Core/Common/Config/Enums.h index 1f6d0a1966..69d8c954c3 100644 --- a/Source/Core/Common/Config/Enums.h +++ b/Source/Core/Common/Config/Enums.h @@ -34,6 +34,7 @@ enum class System FreeLook, Session, GameSettingsOnly, + Achievements, }; constexpr std::array SEARCH_ORDER{{ diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index d456952980..b97185e769 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -847,6 +847,8 @@ static void RebuildUserDirectories(unsigned int dir_index) s_user_paths[F_DUALSHOCKUDPCLIENTCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + DUALSHOCKUDPCLIENT_CONFIG; s_user_paths[F_FREELOOKCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + FREELOOK_CONFIG; + s_user_paths[F_RETROACHIEVEMENTSCONFIG_IDX] = + s_user_paths[D_CONFIG_IDX] + RETROACHIEVEMENTS_CONFIG; s_user_paths[F_MAINLOG_IDX] = s_user_paths[D_LOGS_IDX] + MAIN_LOG; s_user_paths[F_MEM1DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM1_DUMP; s_user_paths[F_MEM2DUMP_IDX] = s_user_paths[D_DUMP_IDX] + MEM2_DUMP; diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index cadabbd541..f6dc396966 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -85,6 +85,7 @@ enum F_DUALSHOCKUDPCLIENTCONFIG_IDX, F_FREELOOKCONFIG_IDX, F_GBABIOS_IDX, + F_RETROACHIEVEMENTSCONFIG_IDX, NUM_PATH_INDICES }; diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp new file mode 100644 index 0000000000..7e0fa5a9d9 --- /dev/null +++ b/Source/Core/Core/AchievementManager.cpp @@ -0,0 +1,114 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef USE_RETRO_ACHIEVEMENTS + +#include "Core/AchievementManager.h" +#include "Common/HttpRequest.h" +#include "Common/WorkQueueThread.h" +#include "Config/AchievementSettings.h" +#include "Core/Core.h" + +AchievementManager* AchievementManager::GetInstance() +{ + static AchievementManager s_instance; + return &s_instance; +} + +void AchievementManager::Init() +{ + if (!m_is_runtime_initialized && Config::Get(Config::RA_ENABLED)) + { + rc_runtime_init(&m_runtime); + m_is_runtime_initialized = true; + m_queue.Reset("AchievementManagerQueue", [](const std::function& func) { func(); }); + LoginAsync("", [](ResponseType r_type) {}); + } +} + +AchievementManager::ResponseType AchievementManager::Login(const std::string& password) +{ + return VerifyCredentials(password); +} + +void AchievementManager::LoginAsync(const std::string& password, const LoginCallback& callback) +{ + m_queue.EmplaceItem([this, password, callback] { callback(VerifyCredentials(password)); }); +} + +bool AchievementManager::IsLoggedIn() const +{ + return m_login_data.response.succeeded; +} + +void AchievementManager::Logout() +{ + Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); + rc_api_destroy_login_response(&m_login_data); + m_login_data.response.succeeded = 0; +} + +void AchievementManager::Shutdown() +{ + m_is_runtime_initialized = false; + m_queue.Shutdown(); + // DON'T log out - keep those credentials for next run. + rc_api_destroy_login_response(&m_login_data); + m_login_data.response.succeeded = 0; + rc_runtime_destroy(&m_runtime); +} + +AchievementManager::ResponseType AchievementManager::VerifyCredentials(const std::string& password) +{ + std::string username = Config::Get(Config::RA_USERNAME); + std::string api_token = Config::Get(Config::RA_API_TOKEN); + rc_api_login_request_t login_request = { + .username = username.c_str(), .api_token = api_token.c_str(), .password = password.c_str()}; + ResponseType r_type = Request( + login_request, &m_login_data, rc_api_init_login_request, rc_api_process_login_response); + if (r_type == ResponseType::SUCCESS) + Config::SetBaseOrCurrent(Config::RA_API_TOKEN, m_login_data.api_token); + return r_type; +} + +// Every RetroAchievements API call, with only a partial exception for fetch_image, follows +// the same design pattern (here, X is the name of the call): +// Create a specific rc_api_X_request_t struct and populate with the necessary values +// Call rc_api_init_X_request to convert this into a generic rc_api_request_t struct +// Perform the HTTP request using the url and post_data in the rc_api_request_t struct +// Call rc_api_process_X_response to convert the raw string HTTP response into a +// rc_api_X_response_t struct +// Use the data in the rc_api_X_response_t struct as needed +// Call rc_api_destroy_X_response when finished with the response struct to free memory +template +AchievementManager::ResponseType AchievementManager::Request( + RcRequest rc_request, RcResponse* rc_response, + const std::function& init_request, + const std::function& process_response) +{ + rc_api_request_t api_request; + Common::HttpRequest http_request; + init_request(&api_request, &rc_request); + auto http_response = http_request.Post(api_request.url, api_request.post_data); + rc_api_destroy_request(&api_request); + if (http_response.has_value() && http_response->size() > 0) + { + const std::string response_str(http_response->begin(), http_response->end()); + process_response(rc_response, response_str.c_str()); + if (rc_response->response.succeeded) + { + return ResponseType::SUCCESS; + } + else + { + Logout(); + return ResponseType::INVALID_CREDENTIALS; + } + } + else + { + return ResponseType::CONNECTION_FAILED; + } +} + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h new file mode 100644 index 0000000000..91fd9339ef --- /dev/null +++ b/Source/Core/Core/AchievementManager.h @@ -0,0 +1,54 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef USE_RETRO_ACHIEVEMENTS +#include +#include +#include +#include + +#include +#include + +#include "Common/Event.h" +#include "Common/WorkQueueThread.h" + +class AchievementManager +{ +public: + enum class ResponseType + { + SUCCESS, + INVALID_CREDENTIALS, + CONNECTION_FAILED, + UNKNOWN_FAILURE + }; + using LoginCallback = std::function; + + static AchievementManager* GetInstance(); + void Init(); + ResponseType Login(const std::string& password); + void LoginAsync(const std::string& password, const LoginCallback& callback); + bool IsLoggedIn() const; + void Logout(); + void Shutdown(); + +private: + AchievementManager() = default; + + ResponseType VerifyCredentials(const std::string& password); + + template + ResponseType Request(RcRequest rc_request, RcResponse* rc_response, + const std::function& init_request, + const std::function& process_response); + + rc_runtime_t m_runtime{}; + bool m_is_runtime_initialized = false; + rc_api_login_response_t m_login_data{}; + Common::WorkQueueThread> m_queue; +}; // class AchievementManager + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 682c686c7d..fcba3cba14 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -1,4 +1,6 @@ add_library(core + AchievementManager.cpp + AchievementManager.h ActionReplay.cpp ActionReplay.h ARDecrypt.cpp @@ -21,6 +23,8 @@ add_library(core CheatSearch.cpp CheatSearch.h CommonTitles.h + Config/AchievementSettings.cpp + Config/AchievementSettings.h Config/DefaultLocale.cpp Config/DefaultLocale.h Config/FreeLookSettings.cpp @@ -747,3 +751,8 @@ if(MSVC) # Add precompiled header target_link_libraries(core PRIVATE use_pch) endif() + +if(USE_RETRO_ACHIEVEMENTS) + target_link_libraries(core PRIVATE rcheevos) + target_compile_definitions(core PRIVATE -DUSE_RETRO_ACHIEVEMENTS) +endif() \ No newline at end of file diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp new file mode 100644 index 0000000000..f7ec642881 --- /dev/null +++ b/Source/Core/Core/Config/AchievementSettings.cpp @@ -0,0 +1,16 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/Config/AchievementSettings.h" + +#include + +#include "Common/Config/Config.h" + +namespace Config +{ +// Configuration Information +const Info RA_ENABLED{{System::Achievements, "Achievements", "Enabled"}, false}; +const Info RA_USERNAME{{System::Achievements, "Achievements", "Username"}, ""}; +const Info RA_API_TOKEN{{System::Achievements, "Achievements", "ApiToken"}, ""}; +} // namespace Config diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h new file mode 100644 index 0000000000..8f26769d7f --- /dev/null +++ b/Source/Core/Core/Config/AchievementSettings.h @@ -0,0 +1,14 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Common/Config/Config.h" + +namespace Config +{ +// Configuration Information +extern const Info RA_ENABLED; +extern const Info RA_USERNAME; +extern const Info RA_API_TOKEN; +} // namespace Config diff --git a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp index acce10521b..6b8ed12e7b 100644 --- a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp +++ b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp @@ -94,6 +94,7 @@ const std::map system_to_ini = { {Config::System::Debugger, F_DEBUGGERCONFIG_IDX}, {Config::System::DualShockUDPClient, F_DUALSHOCKUDPCLIENTCONFIG_IDX}, {Config::System::FreeLook, F_FREELOOKCONFIG_IDX}, + {Config::System::Achievements, F_RETROACHIEVEMENTSCONFIG_IDX}, // Config::System::Session should not be added to this list }; diff --git a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp index d7746baa41..852fbb74ca 100644 --- a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp +++ b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp @@ -7,6 +7,7 @@ #include #include "Common/Config/Config.h" +#include "Core/Config/AchievementSettings.h" #include "Core/Config/GraphicsSettings.h" #include "Core/Config/MainSettings.h" #include "Core/Config/UISettings.h" @@ -37,6 +38,12 @@ bool IsSettingSaveable(const Config::Location& config_location) &Config::WIIMOTE_3_SOURCE.GetLocation(), &Config::WIIMOTE_4_SOURCE.GetLocation(), &Config::WIIMOTE_BB_SOURCE.GetLocation(), + + // Achievements + + &Config::RA_ENABLED.GetLocation(), + &Config::RA_USERNAME.GetLocation(), + &Config::RA_API_TOKEN.GetLocation(), }; return std::any_of(begin(s_setting_saveable), end(s_setting_saveable), diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index c99d353df6..44048669a6 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -161,6 +161,7 @@ + @@ -172,6 +173,7 @@ + @@ -795,6 +797,7 @@ + @@ -805,6 +808,7 @@ + diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj index 5875de1550..827652cae7 100644 --- a/Source/Core/DolphinLib.vcxproj +++ b/Source/Core/DolphinLib.vcxproj @@ -52,6 +52,7 @@ + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index e9d24bc4d1..184ce1db04 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -686,4 +686,9 @@ endif() if(USE_DISCORD_PRESENCE) target_compile_definitions(dolphin-emu PRIVATE -DUSE_DISCORD_PRESENCE) -endif() \ No newline at end of file +endif() + +if(USE_RETRO_ACHIEVEMENTS) + target_link_libraries(dolphin-emu PRIVATE rcheevos) + target_compile_definitions(dolphin-emu PRIVATE -DUSE_RETRO_ACHIEVEMENTS) +endif() diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 838e33d7a1..01280a46a9 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -436,6 +436,7 @@ + diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index ac34999c4d..c1566fb795 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -37,6 +37,7 @@ #include "Common/Version.h" #include "Common/WindowSystemInfo.h" +#include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/CommonTitles.h" @@ -222,6 +223,11 @@ MainWindow::MainWindow(std::unique_ptr boot_parameters, InitControllers(); +#ifdef USE_RETRO_ACHIEVEMENTS + // This has to be done before CreateComponents() so it's initialized. + AchievementManager::GetInstance()->Init(); +#endif // USE_RETRO_ACHIEVEMENTS + CreateComponents(); ConnectGameList(); @@ -301,6 +307,10 @@ MainWindow::~MainWindow() Settings::Instance().ResetNetPlayClient(); Settings::Instance().ResetNetPlayServer(); +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance()->Shutdown(); +#endif // USE_RETRO_ACHIEVEMENTS + delete m_render_widget; delete m_netplay_dialog; diff --git a/Source/VSProps/Base.Dolphin.props b/Source/VSProps/Base.Dolphin.props index 15e3b0f072..6a9d0192d2 100644 --- a/Source/VSProps/Base.Dolphin.props +++ b/Source/VSProps/Base.Dolphin.props @@ -45,6 +45,7 @@ AUTOUPDATE;%(PreprocessorDefinitions) HAVE_SDL2;%(PreprocessorDefinitions) STEAM;%(PreprocessorDefinitions) + USE_RETRO_ACHIEVEMENTS;%(PreprocessorDefinitions)