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)