From a5b7e9012f43f96fafba87439c31e8eadd43a2e8 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Tue, 17 Feb 2026 17:40:58 -0700 Subject: [PATCH 01/12] Add rcheevos library --- .gitmodules | 3 +++ externals/rcheevos/rcheevos | 1 + 2 files changed, 4 insertions(+) create mode 160000 externals/rcheevos/rcheevos diff --git a/.gitmodules b/.gitmodules index 18c5cc79a..ff3fb68e6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -106,3 +106,6 @@ [submodule "externals/libretro-common"] path = externals/libretro-common/libretro-common url = https://github.com/libretro/libretro-common.git +[submodule "externals/rcheevos/rcheevos"] + path = externals/rcheevos/rcheevos + url = https://github.com/RetroAchievements/rcheevos.git diff --git a/externals/rcheevos/rcheevos b/externals/rcheevos/rcheevos new file mode 160000 index 000000000..e9ca3694c --- /dev/null +++ b/externals/rcheevos/rcheevos @@ -0,0 +1 @@ +Subproject commit e9ca3694c862b61235595176dac4b22677848c93 From f993fa3da4c44ef8c48061c0bd1f582c14a74b69 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Tue, 17 Feb 2026 20:53:33 -0700 Subject: [PATCH 02/12] Create rcheevos integration stub --- externals/CMakeLists.txt | 5 +- externals/rcheevos/CMakeLists.txt | 30 ++++++ src/CMakeLists.txt | 1 + src/common/logging/filter.cpp | 3 +- src/common/logging/types.h | 1 + src/core/CMakeLists.txt | 2 +- src/core/core.cpp | 9 +- src/rcheevos_integration/CMakeLists.txt | 6 ++ .../rcheevos_integration.cpp | 102 ++++++++++++++++++ .../rcheevos_integration.h | 8 ++ 10 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 externals/rcheevos/CMakeLists.txt create mode 100644 src/rcheevos_integration/CMakeLists.txt create mode 100644 src/rcheevos_integration/rcheevos_integration.cpp create mode 100644 src/rcheevos_integration/rcheevos_integration.h diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index db8de43a7..02372cc7b 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -513,4 +513,7 @@ elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64|armv8") else() target_compile_definitions(xxhash PRIVATE XXH_VECTOR=XXH_SCALAR) message(STATUS "Disabling SIMD for xxHash") -endif() \ No newline at end of file +endif() + +# rcheevos +add_subdirectory(rcheevos) diff --git a/externals/rcheevos/CMakeLists.txt b/externals/rcheevos/CMakeLists.txt new file mode 100644 index 000000000..53a36194c --- /dev/null +++ b/externals/rcheevos/CMakeLists.txt @@ -0,0 +1,30 @@ +add_library(rcheevos STATIC + rcheevos/src/rc_compat.c + rcheevos/src/rc_client.c + rcheevos/src/rc_util.c + rcheevos/src/rc_version.c + rcheevos/src/rcheevos/alloc.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_validate.c + 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/md5.c + rcheevos/src/rapi/rc_api_common.c + 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 +) + +target_include_directories(rcheevos PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/rcheevos/include +) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bd1810d53..2a7c9d689 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -188,6 +188,7 @@ add_subdirectory(video_core) add_subdirectory(audio_core) add_subdirectory(network) add_subdirectory(input_common) +add_subdirectory(rcheevos_integration) if (ENABLE_TESTS) add_subdirectory(tests) diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index fe905dfe7..fb0ef0243 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -137,7 +137,8 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Movie) \ CLS(Loader) \ CLS(WebService) \ - CLS(RPC_Server) + CLS(RPC_Server) \ + CLS(Rcheevos) // GetClassName is a macro defined by Windows.h, grrr... const char* GetLogClassName(Class log_class) { diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 215bdbb3f..b1115a418 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -106,6 +106,7 @@ enum class Class : u8 { WebService, ///< Interface to Citra Web Services RPC_Server, ///< RPC server Count, ///< Total number of logging classes + Rcheevos, ///< RetroAchievements }; } // namespace Common::Log diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a5abbc585..ef785bc8e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -500,7 +500,7 @@ add_library(citra_core STATIC create_target_directory_groups(citra_core) -target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core) +target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core rcheevos_integration) target_link_libraries(citra_core PRIVATE Boost::boost Boost::serialization Boost::iostreams httplib) target_link_libraries(citra_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng open_source_archives) diff --git a/src/core/core.cpp b/src/core/core.cpp index 00e79ae8b..4e9f812ae 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -50,6 +50,7 @@ #include "core/rpc/server.h" #endif #include "network/network.h" +#include "rcheevos_integration/rcheevos_integration.h" #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" @@ -73,9 +74,13 @@ Core::Timing& Global() { return System::GetInstance().CoreTiming(); } -System::System() : movie{*this}, cheat_engine{*this} {} +System::System() : movie{*this}, cheat_engine{*this} { + initialize_retroachievements_client(); +} -System::~System() = default; +System::~System() { + shutdown_retroachievements_client(); +} System::ResultStatus System::RunLoop(bool tight_loop) { status = ResultStatus::Success; diff --git a/src/rcheevos_integration/CMakeLists.txt b/src/rcheevos_integration/CMakeLists.txt new file mode 100644 index 000000000..e57c2c6fa --- /dev/null +++ b/src/rcheevos_integration/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(rcheevos_integration STATIC + rcheevos_integration.cpp + rcheevos_integration.h +) + +target_link_libraries(rcheevos_integration PUBLIC rcheevos PRIVATE citra_common) diff --git a/src/rcheevos_integration/rcheevos_integration.cpp b/src/rcheevos_integration/rcheevos_integration.cpp new file mode 100644 index 000000000..6c1c6d107 --- /dev/null +++ b/src/rcheevos_integration/rcheevos_integration.cpp @@ -0,0 +1,102 @@ +#include "rcheevos_integration.h" + +#include + +#include "common/logging/log.h" + +rc_client_t* g_client = NULL; + +// This is the function the rc_client will use to read memory for the emulator. we don't need it yet, +// so just provide a dummy function that returns "no memory read". +static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) +{ + // TODO: implement later + LOG_DEBUG(Rcheevos, "Attempting to read memory."); + + return 0; +} + +// // This is the callback function for the asynchronous HTTP call (which is not provided in this example) +// static void http_callback(int status_code, const char* content, size_t content_size, void* userdata, const char* error_message) +// { +// // Prepare a data object to pass the HTTP response to the callback +// rc_api_server_response_t server_response; +// memset(&server_response, 0, sizeof(server_response)); +// server_response.body = content; +// server_response.body_length = content_size; +// server_response.http_status_code = status_code; + +// // handle non-http errors (socket timeout, no internet available, etc) +// if (status_code == 0 && error_message) { +// // assume no server content and pass the error through instead +// server_response.body = error_message; +// server_response.body_length = strlen(error_message); +// // Let rc_client know the error was not catastrophic and could be retried. It may decide to retry or just +// // immediately pass the error to the callback. To prevent possible retries, use RC_API_SERVER_RESPONSE_CLIENT_ERROR. +// server_response.http_status_code = RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR; +// } + +// // Get the rc_client callback and call it +// async_callback_data* async_data = (async_callback_data*)userdata; +// async_data->callback(&server_response, async_data->callback_data); + +// // Release the captured rc_client callback data +// free(async_data); +// } + +// This is the HTTP request dispatcher that is provided to the rc_client. Whenever the client +// needs to talk to the server, it will call this function. +static void server_call(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +{ + LOG_DEBUG(Rcheevos, "Attempting to call server."); + + // // RetroAchievements may not allow hardcore unlocks if we don't properly identify ourselves. + // const char* user_agent = "MyClient/1.2"; + + // // callback must be called with callback_data, regardless of the outcome of the HTTP call. + // // Since we're making the HTTP call asynchronously, we need to capture them and pass it + // // through the async HTTP code. + // async_callback_data* async_data = malloc(sizeof(async_callback_data)); + // async_data->callback = callback; + // async_data->callback_data = callback_data; + + // // If post data is provided, we need to make a POST request, otherwise, a GET request will suffice. + // if (request->post_data) + // async_http_post(request->url, request->post_data, user_agent, http_callback, async_data); + // else + // async_http_get(request->url, user_agent, http_callback, async_data); +} + +// Write log messages to the console +static void log_message(const char* message, const rc_client_t* client) +{ + LOG_DEBUG(Rcheevos, "Rcheevos internal message: \"{}\"", message); +} + +void initialize_retroachievements_client() +{ + LOG_DEBUG(Rcheevos, "Initializing RA client."); + + // Create the client instance (using a global variable simplifies this example) + g_client = rc_client_create(read_memory, server_call); + + // Provide a logging function to simplify debugging + rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); + + // Disable hardcore - if we goof something up in the implementation, we don't want our + // account disabled for cheating. + rc_client_set_hardcore_enabled(g_client, 0); +} + +void shutdown_retroachievements_client() +{ + LOG_DEBUG(Rcheevos, "Shutting down RA client."); + + if (g_client) + { + // Release resources associated to the client instance + rc_client_destroy(g_client); + g_client = NULL; + } +} \ No newline at end of file diff --git a/src/rcheevos_integration/rcheevos_integration.h b/src/rcheevos_integration/rcheevos_integration.h new file mode 100644 index 000000000..eb24925d3 --- /dev/null +++ b/src/rcheevos_integration/rcheevos_integration.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +extern rc_client_t* g_client; + +void initialize_retroachievements_client(); +void shutdown_retroachievements_client(); From 99d64caaf718de8648679a068e38d5c1243afce2 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Tue, 17 Feb 2026 23:54:45 -0700 Subject: [PATCH 03/12] Create wrapper class for rcheevos client and implement user authentication --- src/common/logging/types.h | 2 +- src/core/core.cpp | 12 +- src/core/core.h | 4 + src/rcheevos_integration/CMakeLists.txt | 2 +- .../rcheevos_integration.cpp | 137 +++++++++--------- .../rcheevos_integration.h | 19 ++- 6 files changed, 96 insertions(+), 80 deletions(-) diff --git a/src/common/logging/types.h b/src/common/logging/types.h index b1115a418..97f008d33 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -105,8 +105,8 @@ enum class Class : u8 { Movie, ///< Movie (Input Recording) Playback WebService, ///< Interface to Citra Web Services RPC_Server, ///< RPC server - Count, ///< Total number of logging classes Rcheevos, ///< RetroAchievements + Count, ///< Total number of logging classes }; } // namespace Common::Log diff --git a/src/core/core.cpp b/src/core/core.cpp index 4e9f812ae..f1a4c2724 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -50,7 +50,6 @@ #include "core/rpc/server.h" #endif #include "network/network.h" -#include "rcheevos_integration/rcheevos_integration.h" #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" @@ -74,13 +73,9 @@ Core::Timing& Global() { return System::GetInstance().CoreTiming(); } -System::System() : movie{*this}, cheat_engine{*this} { - initialize_retroachievements_client(); -} +System::System() : movie{*this}, cheat_engine{*this}, rcheevos_client{*this} {} -System::~System() { - shutdown_retroachievements_client(); -} +System::~System() = default; System::ResultStatus System::RunLoop(bool tight_loop) { status = ResultStatus::Success; @@ -596,6 +591,9 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, plg_ldr->SetAllowGameChangeState(Settings::values.allow_plugin_loader.GetValue()); } + rcheevos_client.InitializeClient(); + rcheevos_client.LoginRetroachievementsUser("", ""); + LOG_DEBUG(Core, "Initialized OK"); is_powered_on = true; diff --git a/src/core/core.h b/src/core/core.h index d493e8491..6325fc103 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -18,6 +18,7 @@ #include "core/hle/service/plgldr/plgldr.h" #include "core/movie.h" #include "core/perf_stats.h" +#include "rcheevos_integration/rcheevos_integration.h" namespace Frontend { class EmuWindow; @@ -439,6 +440,9 @@ private: /// Cheats manager Cheats::CheatEngine cheat_engine; + /// RetroAchievements + RcheevosClient rcheevos_client; + /// Video dumper backend std::shared_ptr video_dumper; diff --git a/src/rcheevos_integration/CMakeLists.txt b/src/rcheevos_integration/CMakeLists.txt index e57c2c6fa..4651ec335 100644 --- a/src/rcheevos_integration/CMakeLists.txt +++ b/src/rcheevos_integration/CMakeLists.txt @@ -3,4 +3,4 @@ add_library(rcheevos_integration STATIC rcheevos_integration.h ) -target_link_libraries(rcheevos_integration PUBLIC rcheevos PRIVATE citra_common) +target_link_libraries(rcheevos_integration PRIVATE citra_common httplib rcheevos) diff --git a/src/rcheevos_integration/rcheevos_integration.cpp b/src/rcheevos_integration/rcheevos_integration.cpp index 6c1c6d107..87040e231 100644 --- a/src/rcheevos_integration/rcheevos_integration.cpp +++ b/src/rcheevos_integration/rcheevos_integration.cpp @@ -1,10 +1,14 @@ #include "rcheevos_integration.h" +#include +#include + +#include #include +#include #include "common/logging/log.h" - -rc_client_t* g_client = NULL; +#include "common/scm_rev.h" // This is the function the rc_client will use to read memory for the emulator. we don't need it yet, // so just provide a dummy function that returns "no memory read". @@ -16,56 +20,34 @@ static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_byte return 0; } -// // This is the callback function for the asynchronous HTTP call (which is not provided in this example) -// static void http_callback(int status_code, const char* content, size_t content_size, void* userdata, const char* error_message) -// { -// // Prepare a data object to pass the HTTP response to the callback -// rc_api_server_response_t server_response; -// memset(&server_response, 0, sizeof(server_response)); -// server_response.body = content; -// server_response.body_length = content_size; -// server_response.http_status_code = status_code; - -// // handle non-http errors (socket timeout, no internet available, etc) -// if (status_code == 0 && error_message) { -// // assume no server content and pass the error through instead -// server_response.body = error_message; -// server_response.body_length = strlen(error_message); -// // Let rc_client know the error was not catastrophic and could be retried. It may decide to retry or just -// // immediately pass the error to the callback. To prevent possible retries, use RC_API_SERVER_RESPONSE_CLIENT_ERROR. -// server_response.http_status_code = RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR; -// } - -// // Get the rc_client callback and call it -// async_callback_data* async_data = (async_callback_data*)userdata; -// async_data->callback(&server_response, async_data->callback_data); - -// // Release the captured rc_client callback data -// free(async_data); -// } - -// This is the HTTP request dispatcher that is provided to the rc_client. Whenever the client -// needs to talk to the server, it will call this function. -static void server_call(const rc_api_request_t* request, - rc_client_server_callback_t callback, void* callback_data, rc_client_t* client) +static void server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* rc_client) { LOG_DEBUG(Rcheevos, "Attempting to call server."); - // // RetroAchievements may not allow hardcore unlocks if we don't properly identify ourselves. - // const char* user_agent = "MyClient/1.2"; + std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; // TODO: Make this a numeric version as per https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header - // // callback must be called with callback_data, regardless of the outcome of the HTTP call. - // // Since we're making the HTTP call asynchronously, we need to capture them and pass it - // // through the async HTTP code. - // async_callback_data* async_data = malloc(sizeof(async_callback_data)); - // async_data->callback = callback; - // async_data->callback_data = callback_data; + // TODO: Should make this async? + // TODO: Use a persistent client since base URL will maybe be the same? Or instead just need to parse the URL into scheme-host-port and path. - // // If post data is provided, we need to make a POST request, otherwise, a GET request will suffice. - // if (request->post_data) - // async_http_post(request->url, request->post_data, user_agent, http_callback, async_data); - // else - // async_http_get(request->url, user_agent, http_callback, async_data); + // httplib::Client client(request->url); + httplib::Client client("https://retroachievements.org"); + + httplib::Result result; + if (request->post_data) { + result = client.Post("/dorequest.php", request->post_data, std::strlen(request->post_data), request->content_type); + } else { + result = client.Get("..."); + } + + if (result) { + LOG_DEBUG(Rcheevos, "Status: {}", result->status); + LOG_DEBUG(Rcheevos, "Body: {}", result->body); + + rc_api_server_response_t server_response = { .body = result->body.c_str(), .body_length = result->body.length(), .http_status_code = result->status }; + callback(&server_response, callback_data); + } else { + LOG_DEBUG(Rcheevos, "HTTP error {}", result.error()); + } } // Write log messages to the console @@ -74,29 +56,50 @@ static void log_message(const char* message, const rc_client_t* client) LOG_DEBUG(Rcheevos, "Rcheevos internal message: \"{}\"", message); } -void initialize_retroachievements_client() -{ - LOG_DEBUG(Rcheevos, "Initializing RA client."); +RcheevosClient::RcheevosClient(const Core::System& _system) : system{_system} {} - // Create the client instance (using a global variable simplifies this example) - g_client = rc_client_create(read_memory, server_call); - - // Provide a logging function to simplify debugging - rc_client_enable_logging(g_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); - - // Disable hardcore - if we goof something up in the implementation, we don't want our - // account disabled for cheating. - rc_client_set_hardcore_enabled(g_client, 0); +RcheevosClient::~RcheevosClient() { + if (rc_client) { + rc_client_destroy(rc_client); + rc_client = NULL; + } } -void shutdown_retroachievements_client() -{ - LOG_DEBUG(Rcheevos, "Shutting down RA client."); +void RcheevosClient::InitializeClient() { + LOG_DEBUG(Rcheevos, "Initializing RetroAchievements client."); - if (g_client) + rc_client = rc_client_create(read_memory, server_call); + rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); + rc_client_set_hardcore_enabled(rc_client, 0); +} + +static void login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) +{ + // If not successful, just report the error and bail. + if (result != RC_OK) { - // Release resources associated to the client instance - rc_client_destroy(g_client); - g_client = NULL; + LOG_ERROR(Rcheevos, "Login failed."); + return; } -} \ No newline at end of file + + // Login was successful. Capture the token for future logins so we don't have to store the password anywhere. + const rc_client_user_t* user = rc_client_get_user_info(client); + // store_retroachievements_credentials(user->username, user->token); + + // Inform user of successful login + LOG_INFO(Rcheevos, "Logged in as {} ({} points)", user->display_name, user->score); +} + + +void RcheevosClient::LoginRetroachievementsUser(const char* username, const char* password) +{ + rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); +} + +// void login_remembered_retroachievements_user(const char* username, const char* token) +// { +// // This is exactly the same functionality as rc_client_begin_login_with_password, but +// // uses the token captured from the first login instead of a password. +// // Note that it uses the same callback. +// rc_client_begin_login_with_token(rc_client, username, token, login_callback, NULL); +// } \ No newline at end of file diff --git a/src/rcheevos_integration/rcheevos_integration.h b/src/rcheevos_integration/rcheevos_integration.h index eb24925d3..4eecff4d8 100644 --- a/src/rcheevos_integration/rcheevos_integration.h +++ b/src/rcheevos_integration/rcheevos_integration.h @@ -1,8 +1,19 @@ #pragma once -#include +namespace Core { +class System; +} -extern rc_client_t* g_client; +struct rc_client_t; -void initialize_retroachievements_client(); -void shutdown_retroachievements_client(); +class RcheevosClient { +public: + explicit RcheevosClient(const Core::System& system); + ~RcheevosClient(); + + void InitializeClient(); + void LoginRetroachievementsUser(const char* username, const char* password); +private: + const Core::System& system; + rc_client_t* rc_client = nullptr; +}; From 30beb5d97271526248c37affbedf88e1c671ce53 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 00:44:22 -0700 Subject: [PATCH 04/12] Create a CMake option for toggling RetroAchievements --- CMakeLists.txt | 2 ++ externals/CMakeLists.txt | 4 +++- src/CMakeLists.txt | 6 +++++- src/core/CMakeLists.txt | 6 +++++- src/core/core.cpp | 8 +++++++- src/core/core.h | 2 ++ 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 54eb90e90..7712727a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,6 +136,8 @@ option(ENABLE_SSE42 "Enable SSE4.2 optimizations on x86_64" ON) option(ENABLE_DEVELOPER_OPTIONS "Enable functionality targeted at emulator developers" OFF) +option(ENABLE_RETROACHIEVEMENTS "Build with RetroAchievements support" OFF) + # Compile options CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF) option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO}) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 02372cc7b..e959ba238 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -516,4 +516,6 @@ else() endif() # rcheevos -add_subdirectory(rcheevos) +if (ENABLE_RETROACHIEVEMENTS) + add_subdirectory(rcheevos) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a7c9d689..892495497 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -182,13 +182,17 @@ if(ENABLE_DEVELOPER_OPTIONS) add_compile_definitions(ENABLE_DEVELOPER_OPTIONS) endif() +if (ENABLE_RETROACHIEVEMENTS) + add_compile_definitions(ENABLE_RETROACHIEVEMENTS) + add_subdirectory(rcheevos_integration) +endif() + add_subdirectory(common) add_subdirectory(core) add_subdirectory(video_core) add_subdirectory(audio_core) add_subdirectory(network) add_subdirectory(input_common) -add_subdirectory(rcheevos_integration) if (ENABLE_TESTS) add_subdirectory(tests) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ef785bc8e..a0d4d77e5 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -500,10 +500,14 @@ add_library(citra_core STATIC create_target_directory_groups(citra_core) -target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core rcheevos_integration) +target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core) target_link_libraries(citra_core PRIVATE Boost::boost Boost::serialization Boost::iostreams httplib) target_link_libraries(citra_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng open_source_archives) +if (ENABLE_RETROACHIEVEMENTS) + target_link_libraries(citra_core PUBLIC rcheevos_integration) +endif() + if (ENABLE_WEB_SERVICE) target_link_libraries(citra_core PRIVATE web_service) endif() diff --git a/src/core/core.cpp b/src/core/core.cpp index f1a4c2724..64863d6ed 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -73,7 +73,11 @@ Core::Timing& Global() { return System::GetInstance().CoreTiming(); } -System::System() : movie{*this}, cheat_engine{*this}, rcheevos_client{*this} {} +System::System() : movie{*this}, cheat_engine{*this} +#ifdef ENABLE_RETROACHIEVEMENTS + , rcheevos_client{*this} +#endif +{} System::~System() = default; @@ -591,8 +595,10 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, plg_ldr->SetAllowGameChangeState(Settings::values.allow_plugin_loader.GetValue()); } +#ifdef ENABLE_RETROACHIEVEMENTS rcheevos_client.InitializeClient(); rcheevos_client.LoginRetroachievementsUser("", ""); +#endif LOG_DEBUG(Core, "Initialized OK"); diff --git a/src/core/core.h b/src/core/core.h index 6325fc103..083256a2e 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -440,8 +440,10 @@ private: /// Cheats manager Cheats::CheatEngine cheat_engine; +#ifdef ENABLE_RETROACHIEVEMENTS /// RetroAchievements RcheevosClient rcheevos_client; +#endif /// Video dumper backend std::shared_ptr video_dumper; From 6a737183c0098c823400effc8ca47fd99f11f8c8 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 16:40:08 -0700 Subject: [PATCH 05/12] Add a spot in the UI to authenticate with RetroAchievements --- src/citra_qt/CMakeLists.txt | 4 + .../configuration/configure_general.cpp | 20 +++++ .../configuration/configure_general.h | 1 + .../configuration/configure_general.ui | 74 ++++++++++++++++++- src/core/CMakeLists.txt | 1 + src/core/core.cpp | 25 +++++-- src/core/core.h | 15 +++- .../rcheevos_integration.cpp | 2 +- .../rcheevos_integration.h | 2 +- 9 files changed, 131 insertions(+), 13 deletions(-) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index e6faa88ce..87d5a8082 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -328,3 +328,7 @@ endif() if (CITRA_USE_PRECOMPILED_HEADERS) target_precompile_headers(citra_qt PRIVATE precompiled_headers.h) endif() + +if (ENABLE_RETROACHIEVEMENTS) + target_link_libraries(citra_qt PRIVATE rcheevos_integration) +endif() diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index ca548b21b..a9d72a94f 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -11,6 +11,10 @@ #include "citra_qt/uisettings.h" #include "common/file_util.h" #include "common/settings.h" +#include "core/core.h" +#ifdef ENABLE_RETROACHIEVEMENTS +#include "rcheevos_integration/rcheevos_integration.h" +#endif #include "ui_configure_general.h" // The QSlider doesn't have an easy way to set a custom step amount, @@ -47,6 +51,10 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) ui->updates_group->setVisible(false); #endif +#ifndef ENABLE_RETROACHIEVEMENTS + ui->retro_achievements_group->setVisible(false); +#endif + SetupPerGameUI(); SetConfiguration(); @@ -74,6 +82,9 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) } ui->change_screenshot_dir->setEnabled(true); }); + + connect(ui->retro_achievements_log_in_button, &QPushButton::clicked, this, + &ConfigureGeneral::RetroAchievementsLogIn); } ConfigureGeneral::~ConfigureGeneral() = default; @@ -196,6 +207,15 @@ void ConfigureGeneral::RetranslateUI() { ui->retranslateUi(this); } +void ConfigureGeneral::RetroAchievementsLogIn() { +#ifdef ENABLE_RETROACHIEVEMENTS + std::string username = ui->retro_achievements_username_input->text().toStdString(), + password = ui->retro_achievements_password_input->text().toStdString(); + + Core::System::GetInstance().GetRcheevosClient().LogInRetroachievementsUser(username.c_str(), password.c_str()); +#endif +} + void ConfigureGeneral::SetupPerGameUI() { if (Settings::IsConfiguringGlobal()) { ui->frame_limit->setEnabled(Settings::values.frame_limit.UsingGlobal()); diff --git a/src/citra_qt/configuration/configure_general.h b/src/citra_qt/configuration/configure_general.h index 93a855387..6817506b4 100644 --- a/src/citra_qt/configuration/configure_general.h +++ b/src/citra_qt/configuration/configure_general.h @@ -24,6 +24,7 @@ public: void ApplyConfiguration(); void RetranslateUI(); void SetConfiguration(); + void RetroAchievementsLogIn(); void SetupPerGameUI(); diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index 1c4a05631..01f440eba 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -295,7 +295,7 @@ - + Save Screenshots To @@ -317,6 +317,78 @@ + + + + RetroAchievements + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Username + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Password + + + + + + + + + + + + + Log In + + + + + + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a0d4d77e5..25e9c86b1 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -506,6 +506,7 @@ target_link_libraries(citra_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng ope if (ENABLE_RETROACHIEVEMENTS) target_link_libraries(citra_core PUBLIC rcheevos_integration) + target_compile_definitions(citra_core PUBLIC ENABLE_RETROACHIEVEMENTS) endif() if (ENABLE_WEB_SERVICE) diff --git a/src/core/core.cpp b/src/core/core.cpp index 64863d6ed..d788eca4e 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -50,6 +50,9 @@ #include "core/rpc/server.h" #endif #include "network/network.h" +#ifdef ENABLE_RETROACHIEVEMENTS +#include "rcheevos_integration/rcheevos_integration.h" +#endif #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" @@ -73,11 +76,12 @@ Core::Timing& Global() { return System::GetInstance().CoreTiming(); } -System::System() : movie{*this}, cheat_engine{*this} +System::System() : movie{*this}, cheat_engine{*this} { #ifdef ENABLE_RETROACHIEVEMENTS - , rcheevos_client{*this} + rcheevos_client = std::make_unique(*this); + rcheevos_client->InitializeClient(); #endif -{} +} System::~System() = default; @@ -595,11 +599,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, plg_ldr->SetAllowGameChangeState(Settings::values.allow_plugin_loader.GetValue()); } -#ifdef ENABLE_RETROACHIEVEMENTS - rcheevos_client.InitializeClient(); - rcheevos_client.LoginRetroachievementsUser("", ""); -#endif - LOG_DEBUG(Core, "Initialized OK"); is_powered_on = true; @@ -663,6 +662,16 @@ const Cheats::CheatEngine& System::CheatEngine() const { return cheat_engine; } +#ifdef ENABLE_RETROACHIEVEMENTS +RcheevosClient &System::GetRcheevosClient() { + return *rcheevos_client; +} + +const RcheevosClient &System::GetRcheevosClient() const { + return *rcheevos_client; +} +#endif + void System::RegisterVideoDumper(std::shared_ptr dumper) { video_dumper = std::move(dumper); } diff --git a/src/core/core.h b/src/core/core.h index 083256a2e..15a25d9fd 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -18,7 +18,6 @@ #include "core/hle/service/plgldr/plgldr.h" #include "core/movie.h" #include "core/perf_stats.h" -#include "rcheevos_integration/rcheevos_integration.h" namespace Frontend { class EmuWindow; @@ -71,6 +70,10 @@ namespace Loader { class AppLoader; } +#ifdef ENABLE_RETROACHIEVEMENTS +class RcheevosClient; +#endif + namespace Core { class ARM_Interface; @@ -278,6 +281,14 @@ public: /// Gets a const reference to the cheat engine [[nodiscard]] const Cheats::CheatEngine& CheatEngine() const; +#ifdef ENABLE_RETROACHIEVEMENTS + // Gets a reference to the Rcheevos client + [[nodiscard]] RcheevosClient &GetRcheevosClient(); + + // Gets a const reference to the Rcheevos client + [[nodiscard]] const RcheevosClient &GetRcheevosClient() const; +#endif + /// Gets a reference to the custom texture cache system [[nodiscard]] VideoCore::CustomTexManager& CustomTexManager(); @@ -442,7 +453,7 @@ private: #ifdef ENABLE_RETROACHIEVEMENTS /// RetroAchievements - RcheevosClient rcheevos_client; + std::unique_ptr rcheevos_client; #endif /// Video dumper backend diff --git a/src/rcheevos_integration/rcheevos_integration.cpp b/src/rcheevos_integration/rcheevos_integration.cpp index 87040e231..65d007d2c 100644 --- a/src/rcheevos_integration/rcheevos_integration.cpp +++ b/src/rcheevos_integration/rcheevos_integration.cpp @@ -91,7 +91,7 @@ static void login_callback(int result, const char* error_message, rc_client_t* c } -void RcheevosClient::LoginRetroachievementsUser(const char* username, const char* password) +void RcheevosClient::LogInRetroachievementsUser(const char* username, const char* password) { rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); } diff --git a/src/rcheevos_integration/rcheevos_integration.h b/src/rcheevos_integration/rcheevos_integration.h index 4eecff4d8..4c21850ca 100644 --- a/src/rcheevos_integration/rcheevos_integration.h +++ b/src/rcheevos_integration/rcheevos_integration.h @@ -12,7 +12,7 @@ public: ~RcheevosClient(); void InitializeClient(); - void LoginRetroachievementsUser(const char* username, const char* password); + void LogInRetroachievementsUser(const char* username, const char* password); private: const Core::System& system; rc_client_t* rc_client = nullptr; From b89d93a8e8eb7be7d731079bd9cfdf1b352d1cb0 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 17:48:26 -0700 Subject: [PATCH 06/12] Rename public-facing RA stuff from rcheevos to just RA --- src/CMakeLists.txt | 2 +- src/citra_qt/CMakeLists.txt | 2 +- .../configuration/configure_general.cpp | 12 +- .../configuration/configure_general.ui | 12 +- src/common/logging/filter.cpp | 2 +- src/common/logging/types.h | 110 +++++++++--------- src/core/CMakeLists.txt | 2 +- src/core/core.cpp | 14 +-- src/core/core.h | 14 ++- src/rcheevos_integration/CMakeLists.txt | 6 - .../rcheevos_integration.h | 19 --- src/retroachievements/CMakeLists.txt | 6 + .../client.cpp} | 33 +++--- src/retroachievements/client.h | 23 ++++ 14 files changed, 133 insertions(+), 124 deletions(-) delete mode 100644 src/rcheevos_integration/CMakeLists.txt delete mode 100644 src/rcheevos_integration/rcheevos_integration.h create mode 100644 src/retroachievements/CMakeLists.txt rename src/{rcheevos_integration/rcheevos_integration.cpp => retroachievements/client.cpp} (76%) create mode 100644 src/retroachievements/client.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 892495497..f43e5807b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -184,7 +184,7 @@ endif() if (ENABLE_RETROACHIEVEMENTS) add_compile_definitions(ENABLE_RETROACHIEVEMENTS) - add_subdirectory(rcheevos_integration) + add_subdirectory(retroachievements) endif() add_subdirectory(common) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 87d5a8082..1e7671550 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -330,5 +330,5 @@ if (CITRA_USE_PRECOMPILED_HEADERS) endif() if (ENABLE_RETROACHIEVEMENTS) - target_link_libraries(citra_qt PRIVATE rcheevos_integration) + target_link_libraries(citra_qt PRIVATE retroachievements) endif() diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index a9d72a94f..35956b4e1 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -13,7 +13,7 @@ #include "common/settings.h" #include "core/core.h" #ifdef ENABLE_RETROACHIEVEMENTS -#include "rcheevos_integration/rcheevos_integration.h" +#include "retroachievements/client.h" #endif #include "ui_configure_general.h" @@ -52,7 +52,7 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) #endif #ifndef ENABLE_RETROACHIEVEMENTS - ui->retro_achievements_group->setVisible(false); + ui->retroachievements_group->setVisible(false); #endif SetupPerGameUI(); @@ -83,7 +83,7 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) ui->change_screenshot_dir->setEnabled(true); }); - connect(ui->retro_achievements_log_in_button, &QPushButton::clicked, this, + connect(ui->retroachievements_log_in_button, &QPushButton::clicked, this, &ConfigureGeneral::RetroAchievementsLogIn); } @@ -209,10 +209,10 @@ void ConfigureGeneral::RetranslateUI() { void ConfigureGeneral::RetroAchievementsLogIn() { #ifdef ENABLE_RETROACHIEVEMENTS - std::string username = ui->retro_achievements_username_input->text().toStdString(), - password = ui->retro_achievements_password_input->text().toStdString(); + std::string username = ui->retroachievements_username_input->text().toStdString(), + password = ui->retroachievements_password_input->text().toStdString(); - Core::System::GetInstance().GetRcheevosClient().LogInRetroachievementsUser(username.c_str(), password.c_str()); + Core::System::GetInstance().RetroAchievementsClient().LogInUser(username.c_str(), password.c_str()); #endif } diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index 01f440eba..6741be317 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -318,13 +318,13 @@ - + RetroAchievements - + 0 @@ -346,13 +346,13 @@ - + - + 0 @@ -374,13 +374,13 @@ - + - + Log In diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index fb0ef0243..e4710c91f 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -138,7 +138,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Loader) \ CLS(WebService) \ CLS(RPC_Server) \ - CLS(Rcheevos) + CLS(RetroAchievements) // GetClassName is a macro defined by Windows.h, grrr... const char* GetLogClassName(Class log_class) { diff --git a/src/common/logging/types.h b/src/common/logging/types.h index 97f008d33..9a72cc5a4 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -52,61 +52,61 @@ enum class Class : u8 { Applet_SWKBD, ///< The Software Keyboard applet Service, ///< HLE implementation of system services. Each major service ///< should have its own subclass. - Service_SRV, ///< The SRV (Service Directory) implementation - Service_FRD, ///< The FRD (Friends) service - Service_FS, ///< The FS (Filesystem) service implementation - Service_ERR, ///< The ERR (Error) port implementation - Service_ACT, ///< The ACT (Account) service - Service_APT, ///< The APT (Applets) service - Service_BOSS, ///< The BOSS (SpotPass) service - Service_GSP, ///< The GSP (GPU control) service - Service_AC, ///< The AC (WiFi status) service - Service_AM, ///< The AM (Application manager) service - Service_PTM, ///< The PTM (Power status & misc.) service - Service_LDR, ///< The LDR (3ds dll loader) service - Service_MIC, ///< The MIC (Microphone) service - Service_NDM, ///< The NDM (Network daemon manager) service - Service_NFC, ///< The NFC service - Service_NIM, ///< The NIM (Network interface manager) service - Service_NS, ///< The NS (Nintendo User Interface Shell) service - Service_NWM, ///< The NWM (Network wlan manager) service - Service_CAM, ///< The CAM (Camera) service - Service_CECD, ///< The CECD (StreetPass) service - Service_CFG, ///< The CFG (Configuration) service - Service_CSND, ///< The CSND (CWAV format process) service - Service_DSP, ///< The DSP (DSP control) service - Service_DLP, ///< The DLP (Download Play) service - Service_HID, ///< The HID (Human interface device) service - Service_HTTP, ///< The HTTP service - Service_SOC, ///< The SOC (Socket) service - Service_IR, ///< The IR service - Service_Y2R, ///< The Y2R (YUV to RGB conversion) service - Service_PS, ///< The PS (Process) service - Service_PLGLDR, ///< The PLGLDR (plugin loader) service - Service_NEWS, ///< The NEWS (Notifications) service - HW, ///< Low-level hardware emulation - HW_Memory, ///< Memory-map and address translation - HW_LCD, ///< LCD register emulation - HW_GPU, ///< GPU control emulation - HW_AES, ///< AES engine emulation - HW_RSA, ///< RSA engine emulation - HW_ECC, ///< ECC engine emulation - Frontend, ///< Emulator UI - Render, ///< Emulator video output and hardware acceleration - Render_Software, ///< Software renderer backend - Render_OpenGL, ///< OpenGL backend - Render_Vulkan, ///< Vulkan backend - Audio, ///< Audio emulation - Audio_DSP, ///< The HLE and LLE implementations of the DSP - Audio_Sink, ///< Emulator audio output backend - Loader, ///< ROM loader - Input, ///< Input emulation - Network, ///< Network emulation - Movie, ///< Movie (Input Recording) Playback - WebService, ///< Interface to Citra Web Services - RPC_Server, ///< RPC server - Rcheevos, ///< RetroAchievements - Count, ///< Total number of logging classes + Service_SRV, ///< The SRV (Service Directory) implementation + Service_FRD, ///< The FRD (Friends) service + Service_FS, ///< The FS (Filesystem) service implementation + Service_ERR, ///< The ERR (Error) port implementation + Service_ACT, ///< The ACT (Account) service + Service_APT, ///< The APT (Applets) service + Service_BOSS, ///< The BOSS (SpotPass) service + Service_GSP, ///< The GSP (GPU control) service + Service_AC, ///< The AC (WiFi status) service + Service_AM, ///< The AM (Application manager) service + Service_PTM, ///< The PTM (Power status & misc.) service + Service_LDR, ///< The LDR (3ds dll loader) service + Service_MIC, ///< The MIC (Microphone) service + Service_NDM, ///< The NDM (Network daemon manager) service + Service_NFC, ///< The NFC service + Service_NIM, ///< The NIM (Network interface manager) service + Service_NS, ///< The NS (Nintendo User Interface Shell) service + Service_NWM, ///< The NWM (Network wlan manager) service + Service_CAM, ///< The CAM (Camera) service + Service_CECD, ///< The CECD (StreetPass) service + Service_CFG, ///< The CFG (Configuration) service + Service_CSND, ///< The CSND (CWAV format process) service + Service_DSP, ///< The DSP (DSP control) service + Service_DLP, ///< The DLP (Download Play) service + Service_HID, ///< The HID (Human interface device) service + Service_HTTP, ///< The HTTP service + Service_SOC, ///< The SOC (Socket) service + Service_IR, ///< The IR service + Service_Y2R, ///< The Y2R (YUV to RGB conversion) service + Service_PS, ///< The PS (Process) service + Service_PLGLDR, ///< The PLGLDR (plugin loader) service + Service_NEWS, ///< The NEWS (Notifications) service + HW, ///< Low-level hardware emulation + HW_Memory, ///< Memory-map and address translation + HW_LCD, ///< LCD register emulation + HW_GPU, ///< GPU control emulation + HW_AES, ///< AES engine emulation + HW_RSA, ///< RSA engine emulation + HW_ECC, ///< ECC engine emulation + Frontend, ///< Emulator UI + Render, ///< Emulator video output and hardware acceleration + Render_Software, ///< Software renderer backend + Render_OpenGL, ///< OpenGL backend + Render_Vulkan, ///< Vulkan backend + Audio, ///< Audio emulation + Audio_DSP, ///< The HLE and LLE implementations of the DSP + Audio_Sink, ///< Emulator audio output backend + Loader, ///< ROM loader + Input, ///< Input emulation + Network, ///< Network emulation + Movie, ///< Movie (Input Recording) Playback + WebService, ///< Interface to Citra Web Services + RPC_Server, ///< RPC server + RetroAchievements, ///< RetroAchievements + Count, ///< Total number of logging classes }; } // namespace Common::Log diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 25e9c86b1..95037d61d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -505,7 +505,7 @@ target_link_libraries(citra_core PRIVATE Boost::boost Boost::serialization Boost target_link_libraries(citra_core PUBLIC dds-ktx PRIVATE cryptopp fmt lodepng open_source_archives) if (ENABLE_RETROACHIEVEMENTS) - target_link_libraries(citra_core PUBLIC rcheevos_integration) + target_link_libraries(citra_core PUBLIC retroachievements) target_compile_definitions(citra_core PUBLIC ENABLE_RETROACHIEVEMENTS) endif() diff --git a/src/core/core.cpp b/src/core/core.cpp index d788eca4e..707e859af 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -51,7 +51,7 @@ #endif #include "network/network.h" #ifdef ENABLE_RETROACHIEVEMENTS -#include "rcheevos_integration/rcheevos_integration.h" +#include "retroachievements/client.h" #endif #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/gpu.h" @@ -78,8 +78,8 @@ Core::Timing& Global() { System::System() : movie{*this}, cheat_engine{*this} { #ifdef ENABLE_RETROACHIEVEMENTS - rcheevos_client = std::make_unique(*this); - rcheevos_client->InitializeClient(); + retroachievements_client = std::make_unique(*this); + retroachievements_client->Initialize(); #endif } @@ -663,12 +663,12 @@ const Cheats::CheatEngine& System::CheatEngine() const { } #ifdef ENABLE_RETROACHIEVEMENTS -RcheevosClient &System::GetRcheevosClient() { - return *rcheevos_client; +RetroAchievements::Client &System::RetroAchievementsClient() { + return *retroachievements_client; } -const RcheevosClient &System::GetRcheevosClient() const { - return *rcheevos_client; +const RetroAchievements::Client &System::RetroAchievementsClient() const { + return *retroachievements_client; } #endif diff --git a/src/core/core.h b/src/core/core.h index 15a25d9fd..b261a1a88 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -71,7 +71,9 @@ class AppLoader; } #ifdef ENABLE_RETROACHIEVEMENTS -class RcheevosClient; +namespace RetroAchievements { +class Client; +} // namespace RetroAchievements #endif namespace Core { @@ -282,11 +284,11 @@ public: [[nodiscard]] const Cheats::CheatEngine& CheatEngine() const; #ifdef ENABLE_RETROACHIEVEMENTS - // Gets a reference to the Rcheevos client - [[nodiscard]] RcheevosClient &GetRcheevosClient(); + // Gets a reference to the RetroAchievements client + [[nodiscard]] RetroAchievements::Client &RetroAchievementsClient(); - // Gets a const reference to the Rcheevos client - [[nodiscard]] const RcheevosClient &GetRcheevosClient() const; + // Gets a const reference to the RetroAchievements client + [[nodiscard]] const RetroAchievements::Client &RetroAchievementsClient() const; #endif /// Gets a reference to the custom texture cache system @@ -453,7 +455,7 @@ private: #ifdef ENABLE_RETROACHIEVEMENTS /// RetroAchievements - std::unique_ptr rcheevos_client; + std::unique_ptr retroachievements_client; #endif /// Video dumper backend diff --git a/src/rcheevos_integration/CMakeLists.txt b/src/rcheevos_integration/CMakeLists.txt deleted file mode 100644 index 4651ec335..000000000 --- a/src/rcheevos_integration/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -add_library(rcheevos_integration STATIC - rcheevos_integration.cpp - rcheevos_integration.h -) - -target_link_libraries(rcheevos_integration PRIVATE citra_common httplib rcheevos) diff --git a/src/rcheevos_integration/rcheevos_integration.h b/src/rcheevos_integration/rcheevos_integration.h deleted file mode 100644 index 4c21850ca..000000000 --- a/src/rcheevos_integration/rcheevos_integration.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -namespace Core { -class System; -} - -struct rc_client_t; - -class RcheevosClient { -public: - explicit RcheevosClient(const Core::System& system); - ~RcheevosClient(); - - void InitializeClient(); - void LogInRetroachievementsUser(const char* username, const char* password); -private: - const Core::System& system; - rc_client_t* rc_client = nullptr; -}; diff --git a/src/retroachievements/CMakeLists.txt b/src/retroachievements/CMakeLists.txt new file mode 100644 index 000000000..86b97b2d2 --- /dev/null +++ b/src/retroachievements/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(retroachievements STATIC + client.cpp + client.h +) + +target_link_libraries(retroachievements PRIVATE citra_common httplib rcheevos) diff --git a/src/rcheevos_integration/rcheevos_integration.cpp b/src/retroachievements/client.cpp similarity index 76% rename from src/rcheevos_integration/rcheevos_integration.cpp rename to src/retroachievements/client.cpp index 65d007d2c..ea5a8676f 100644 --- a/src/rcheevos_integration/rcheevos_integration.cpp +++ b/src/retroachievements/client.cpp @@ -1,4 +1,4 @@ -#include "rcheevos_integration.h" +#include "client.h" #include #include @@ -10,19 +10,21 @@ #include "common/logging/log.h" #include "common/scm_rev.h" +namespace RetroAchievements +{ // This is the function the rc_client will use to read memory for the emulator. we don't need it yet, // so just provide a dummy function that returns "no memory read". static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) { // TODO: implement later - LOG_DEBUG(Rcheevos, "Attempting to read memory."); + LOG_DEBUG(RetroAchievements, "Attempting to read memory."); return 0; } static void server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* rc_client) { - LOG_DEBUG(Rcheevos, "Attempting to call server."); + LOG_DEBUG(RetroAchievements, "Attempting to call server."); std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; // TODO: Make this a numeric version as per https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header @@ -40,33 +42,33 @@ static void server_call(const rc_api_request_t* request, rc_client_server_callba } if (result) { - LOG_DEBUG(Rcheevos, "Status: {}", result->status); - LOG_DEBUG(Rcheevos, "Body: {}", result->body); + LOG_DEBUG(RetroAchievements, "Status: {}", result->status); + LOG_DEBUG(RetroAchievements, "Body: {}", result->body); rc_api_server_response_t server_response = { .body = result->body.c_str(), .body_length = result->body.length(), .http_status_code = result->status }; callback(&server_response, callback_data); } else { - LOG_DEBUG(Rcheevos, "HTTP error {}", result.error()); + LOG_DEBUG(RetroAchievements, "HTTP error {}", result.error()); } } // Write log messages to the console static void log_message(const char* message, const rc_client_t* client) { - LOG_DEBUG(Rcheevos, "Rcheevos internal message: \"{}\"", message); + LOG_DEBUG(RetroAchievements, "RetroAchievements internal message: \"{}\"", message); } -RcheevosClient::RcheevosClient(const Core::System& _system) : system{_system} {} +Client::Client(const Core::System& _system) : system{_system} {} -RcheevosClient::~RcheevosClient() { +Client::~Client() { if (rc_client) { rc_client_destroy(rc_client); rc_client = NULL; } } -void RcheevosClient::InitializeClient() { - LOG_DEBUG(Rcheevos, "Initializing RetroAchievements client."); +void Client::Initialize() { + LOG_DEBUG(RetroAchievements, "Initializing RetroAchievements client."); rc_client = rc_client_create(read_memory, server_call); rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); @@ -78,7 +80,7 @@ static void login_callback(int result, const char* error_message, rc_client_t* c // If not successful, just report the error and bail. if (result != RC_OK) { - LOG_ERROR(Rcheevos, "Login failed."); + LOG_ERROR(RetroAchievements, "Login failed."); return; } @@ -87,11 +89,11 @@ static void login_callback(int result, const char* error_message, rc_client_t* c // store_retroachievements_credentials(user->username, user->token); // Inform user of successful login - LOG_INFO(Rcheevos, "Logged in as {} ({} points)", user->display_name, user->score); + LOG_INFO(RetroAchievements, "Logged in as {} ({} points)", user->display_name, user->score); } -void RcheevosClient::LogInRetroachievementsUser(const char* username, const char* password) +void Client::LogInUser(const char* username, const char* password) { rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); } @@ -102,4 +104,5 @@ void RcheevosClient::LogInRetroachievementsUser(const char* username, const char // // uses the token captured from the first login instead of a password. // // Note that it uses the same callback. // rc_client_begin_login_with_token(rc_client, username, token, login_callback, NULL); -// } \ No newline at end of file +// } +} // namespace RetroAchievements diff --git a/src/retroachievements/client.h b/src/retroachievements/client.h new file mode 100644 index 000000000..7e5dbd9f3 --- /dev/null +++ b/src/retroachievements/client.h @@ -0,0 +1,23 @@ +#pragma once + +namespace Core { +class System; +} + +struct rc_client_t; + +namespace RetroAchievements +{ + class Client { + public: + explicit Client(const Core::System& system); + ~Client(); + + void Initialize(); + + void LogInUser(const char* username, const char* password); + private: + const Core::System& system; + rc_client_t* rc_client = nullptr; + }; +} // namespace RetroAchievements From 0e5d0a341896529dedf24d79516ffbda1213c14c Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 17:51:04 -0700 Subject: [PATCH 07/12] Clean up client code a bit --- src/retroachievements/client.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/retroachievements/client.cpp b/src/retroachievements/client.cpp index ea5a8676f..748512967 100644 --- a/src/retroachievements/client.cpp +++ b/src/retroachievements/client.cpp @@ -12,6 +12,7 @@ namespace RetroAchievements { +namespace Callbacks { // This is the function the rc_client will use to read memory for the emulator. we don't need it yet, // so just provide a dummy function that returns "no memory read". static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) @@ -57,6 +58,7 @@ static void log_message(const char* message, const rc_client_t* client) { LOG_DEBUG(RetroAchievements, "RetroAchievements internal message: \"{}\"", message); } +} Client::Client(const Core::System& _system) : system{_system} {} @@ -70,8 +72,8 @@ Client::~Client() { void Client::Initialize() { LOG_DEBUG(RetroAchievements, "Initializing RetroAchievements client."); - rc_client = rc_client_create(read_memory, server_call); - rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, log_message); + rc_client = rc_client_create(Callbacks::read_memory, Callbacks::server_call); + rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, Callbacks::log_message); rc_client_set_hardcore_enabled(rc_client, 0); } @@ -97,12 +99,4 @@ void Client::LogInUser(const char* username, const char* password) { rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); } - -// void login_remembered_retroachievements_user(const char* username, const char* token) -// { -// // This is exactly the same functionality as rc_client_begin_login_with_password, but -// // uses the token captured from the first login instead of a password. -// // Note that it uses the same callback. -// rc_client_begin_login_with_token(rc_client, username, token, login_callback, NULL); -// } } // namespace RetroAchievements From d207e80a519cea94c5ebe405fb18083f3cc0d5b9 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 17:52:38 -0700 Subject: [PATCH 08/12] Run clang-format --- .../configuration/configure_general.cpp | 5 +- src/common/logging/filter.cpp | 2 +- src/core/core.cpp | 4 +- src/core/core.h | 4 +- src/retroachievements/client.cpp | 124 +++++++++--------- src/retroachievements/client.h | 24 ++-- 6 files changed, 84 insertions(+), 79 deletions(-) diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index 35956b4e1..e770ea233 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -210,9 +210,10 @@ void ConfigureGeneral::RetranslateUI() { void ConfigureGeneral::RetroAchievementsLogIn() { #ifdef ENABLE_RETROACHIEVEMENTS std::string username = ui->retroachievements_username_input->text().toStdString(), - password = ui->retroachievements_password_input->text().toStdString(); + password = ui->retroachievements_password_input->text().toStdString(); - Core::System::GetInstance().RetroAchievementsClient().LogInUser(username.c_str(), password.c_str()); + Core::System::GetInstance().RetroAchievementsClient().LogInUser(username.c_str(), + password.c_str()); #endif } diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index e4710c91f..c951a5b1c 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -137,7 +137,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) { CLS(Movie) \ CLS(Loader) \ CLS(WebService) \ - CLS(RPC_Server) \ + CLS(RPC_Server) \ CLS(RetroAchievements) // GetClassName is a macro defined by Windows.h, grrr... diff --git a/src/core/core.cpp b/src/core/core.cpp index 707e859af..e55eeb77b 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -663,11 +663,11 @@ const Cheats::CheatEngine& System::CheatEngine() const { } #ifdef ENABLE_RETROACHIEVEMENTS -RetroAchievements::Client &System::RetroAchievementsClient() { +RetroAchievements::Client& System::RetroAchievementsClient() { return *retroachievements_client; } -const RetroAchievements::Client &System::RetroAchievementsClient() const { +const RetroAchievements::Client& System::RetroAchievementsClient() const { return *retroachievements_client; } #endif diff --git a/src/core/core.h b/src/core/core.h index b261a1a88..73c520836 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -285,10 +285,10 @@ public: #ifdef ENABLE_RETROACHIEVEMENTS // Gets a reference to the RetroAchievements client - [[nodiscard]] RetroAchievements::Client &RetroAchievementsClient(); + [[nodiscard]] RetroAchievements::Client& RetroAchievementsClient(); // Gets a const reference to the RetroAchievements client - [[nodiscard]] const RetroAchievements::Client &RetroAchievementsClient() const; + [[nodiscard]] const RetroAchievements::Client& RetroAchievementsClient() const; #endif /// Gets a reference to the custom texture cache system diff --git a/src/retroachievements/client.cpp b/src/retroachievements/client.cpp index 748512967..fe51db989 100644 --- a/src/retroachievements/client.cpp +++ b/src/retroachievements/client.cpp @@ -10,93 +10,97 @@ #include "common/logging/log.h" #include "common/scm_rev.h" -namespace RetroAchievements -{ +namespace RetroAchievements { namespace Callbacks { -// This is the function the rc_client will use to read memory for the emulator. we don't need it yet, -// so just provide a dummy function that returns "no memory read". -static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client) -{ - // TODO: implement later - LOG_DEBUG(RetroAchievements, "Attempting to read memory."); +// This is the function the rc_client will use to read memory for the emulator. we don't need it +// yet, so just provide a dummy function that returns "no memory read". +static uint32_t read_memory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, + rc_client_t* client) { + // TODO: implement later + LOG_DEBUG(RetroAchievements, "Attempting to read memory."); - return 0; + return 0; } -static void server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data, rc_client_t* rc_client) -{ - LOG_DEBUG(RetroAchievements, "Attempting to call server."); +static void server_call(const rc_api_request_t* request, rc_client_server_callback_t callback, + void* callback_data, rc_client_t* rc_client) { + LOG_DEBUG(RetroAchievements, "Attempting to call server."); - std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; // TODO: Make this a numeric version as per https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header + std::string user_agent = + std::string("Azahar/") + + Common:: + g_build_fullname; // TODO: Make this a numeric version as per + // https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header - // TODO: Should make this async? - // TODO: Use a persistent client since base URL will maybe be the same? Or instead just need to parse the URL into scheme-host-port and path. + // TODO: Should make this async? + // TODO: Use a persistent client since base URL will maybe be the same? Or instead just need to + // parse the URL into scheme-host-port and path. - // httplib::Client client(request->url); - httplib::Client client("https://retroachievements.org"); + // httplib::Client client(request->url); + httplib::Client client("https://retroachievements.org"); - httplib::Result result; - if (request->post_data) { - result = client.Post("/dorequest.php", request->post_data, std::strlen(request->post_data), request->content_type); - } else { - result = client.Get("..."); - } + httplib::Result result; + if (request->post_data) { + result = client.Post("/dorequest.php", request->post_data, std::strlen(request->post_data), + request->content_type); + } else { + result = client.Get("..."); + } - if (result) { - LOG_DEBUG(RetroAchievements, "Status: {}", result->status); - LOG_DEBUG(RetroAchievements, "Body: {}", result->body); + if (result) { + LOG_DEBUG(RetroAchievements, "Status: {}", result->status); + LOG_DEBUG(RetroAchievements, "Body: {}", result->body); - rc_api_server_response_t server_response = { .body = result->body.c_str(), .body_length = result->body.length(), .http_status_code = result->status }; - callback(&server_response, callback_data); - } else { - LOG_DEBUG(RetroAchievements, "HTTP error {}", result.error()); - } + rc_api_server_response_t server_response = {.body = result->body.c_str(), + .body_length = result->body.length(), + .http_status_code = result->status}; + callback(&server_response, callback_data); + } else { + LOG_DEBUG(RetroAchievements, "HTTP error {}", result.error()); + } } // Write log messages to the console -static void log_message(const char* message, const rc_client_t* client) -{ - LOG_DEBUG(RetroAchievements, "RetroAchievements internal message: \"{}\"", message); -} +static void log_message(const char* message, const rc_client_t* client) { + LOG_DEBUG(RetroAchievements, "RetroAchievements internal message: \"{}\"", message); } +} // namespace Callbacks Client::Client(const Core::System& _system) : system{_system} {} Client::~Client() { - if (rc_client) { - rc_client_destroy(rc_client); - rc_client = NULL; - } + if (rc_client) { + rc_client_destroy(rc_client); + rc_client = NULL; + } } void Client::Initialize() { - LOG_DEBUG(RetroAchievements, "Initializing RetroAchievements client."); + LOG_DEBUG(RetroAchievements, "Initializing RetroAchievements client."); - rc_client = rc_client_create(Callbacks::read_memory, Callbacks::server_call); - rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, Callbacks::log_message); - rc_client_set_hardcore_enabled(rc_client, 0); + rc_client = rc_client_create(Callbacks::read_memory, Callbacks::server_call); + rc_client_enable_logging(rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, Callbacks::log_message); + rc_client_set_hardcore_enabled(rc_client, 0); } -static void login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) -{ - // If not successful, just report the error and bail. - if (result != RC_OK) - { - LOG_ERROR(RetroAchievements, "Login failed."); - return; - } +static void login_callback(int result, const char* error_message, rc_client_t* client, + void* userdata) { + // If not successful, just report the error and bail. + if (result != RC_OK) { + LOG_ERROR(RetroAchievements, "Login failed."); + return; + } - // Login was successful. Capture the token for future logins so we don't have to store the password anywhere. - const rc_client_user_t* user = rc_client_get_user_info(client); - // store_retroachievements_credentials(user->username, user->token); + // Login was successful. Capture the token for future logins so we don't have to store the + // password anywhere. + const rc_client_user_t* user = rc_client_get_user_info(client); + // store_retroachievements_credentials(user->username, user->token); - // Inform user of successful login - LOG_INFO(RetroAchievements, "Logged in as {} ({} points)", user->display_name, user->score); + // Inform user of successful login + LOG_INFO(RetroAchievements, "Logged in as {} ({} points)", user->display_name, user->score); } - -void Client::LogInUser(const char* username, const char* password) -{ - rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); +void Client::LogInUser(const char* username, const char* password) { + rc_client_begin_login_with_password(rc_client, username, password, login_callback, NULL); } } // namespace RetroAchievements diff --git a/src/retroachievements/client.h b/src/retroachievements/client.h index 7e5dbd9f3..c8005b7e6 100644 --- a/src/retroachievements/client.h +++ b/src/retroachievements/client.h @@ -6,18 +6,18 @@ class System; struct rc_client_t; -namespace RetroAchievements -{ - class Client { - public: - explicit Client(const Core::System& system); - ~Client(); +namespace RetroAchievements { +class Client { +public: + explicit Client(const Core::System& system); + ~Client(); - void Initialize(); + void Initialize(); - void LogInUser(const char* username, const char* password); - private: - const Core::System& system; - rc_client_t* rc_client = nullptr; - }; + void LogInUser(const char* username, const char* password); + +private: + const Core::System& system; + rc_client_t* rc_client = nullptr; +}; } // namespace RetroAchievements From af792ce805c98b68beabdbc4574044485b1ed358 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 18 Feb 2026 18:06:52 -0700 Subject: [PATCH 09/12] Hide password --- src/citra_qt/configuration/configure_general.ui | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index 6741be317..9468d0c35 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -374,7 +374,11 @@ - + + + QLineEdit::PasswordEchoOnEdit + + From 58c326ef0600c627ab4040c0010ff44cedee487a Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Thu, 19 Feb 2026 10:22:13 -0700 Subject: [PATCH 10/12] Fix weirdly-formatted comment --- src/retroachievements/client.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/retroachievements/client.cpp b/src/retroachievements/client.cpp index fe51db989..f162c8313 100644 --- a/src/retroachievements/client.cpp +++ b/src/retroachievements/client.cpp @@ -26,11 +26,9 @@ static void server_call(const rc_api_request_t* request, rc_client_server_callba void* callback_data, rc_client_t* rc_client) { LOG_DEBUG(RetroAchievements, "Attempting to call server."); - std::string user_agent = - std::string("Azahar/") + - Common:: - g_build_fullname; // TODO: Make this a numeric version as per - // https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header + // TODO: Make this a numeric version as per + // https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header + std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; // TODO: Should make this async? // TODO: Use a persistent client since base URL will maybe be the same? Or instead just need to From aaaa890f10a83cc9ca619c875c065dae710dfeb8 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Thu, 19 Feb 2026 11:44:54 -0700 Subject: [PATCH 11/12] Improve server call and actually use User-Agent header --- src/retroachievements/client.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/retroachievements/client.cpp b/src/retroachievements/client.cpp index f162c8313..43588b996 100644 --- a/src/retroachievements/client.cpp +++ b/src/retroachievements/client.cpp @@ -11,6 +11,13 @@ #include "common/scm_rev.h" namespace RetroAchievements { +static const char base_url[] = "https://retroachievements.org"; + +// TODO: Make this use a numeric version as per +// https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header +static const std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; +static const httplib::Headers headers = httplib::Headers({{"User-Agent", user_agent}}); + namespace Callbacks { // This is the function the rc_client will use to read memory for the emulator. we don't need it // yet, so just provide a dummy function that returns "no memory read". @@ -26,23 +33,21 @@ static void server_call(const rc_api_request_t* request, rc_client_server_callba void* callback_data, rc_client_t* rc_client) { LOG_DEBUG(RetroAchievements, "Attempting to call server."); - // TODO: Make this a numeric version as per - // https://github.com/RetroAchievements/rcheevos/wiki/rc_client-integration#user-agent-header - std::string user_agent = std::string("Azahar/") + Common::g_build_fullname; + if (std::strstr(request->url, base_url) == nullptr) { + LOG_ERROR(RetroAchievements, "Invalid URL: \"{}\"", request->url); + return; + } + + httplib::Client client(base_url); + const char* path = &request->url[sizeof(base_url) - 1]; // TODO: Should make this async? - // TODO: Use a persistent client since base URL will maybe be the same? Or instead just need to - // parse the URL into scheme-host-port and path. - - // httplib::Client client(request->url); - httplib::Client client("https://retroachievements.org"); - httplib::Result result; if (request->post_data) { - result = client.Post("/dorequest.php", request->post_data, std::strlen(request->post_data), + result = client.Post(path, headers, request->post_data, std::strlen(request->post_data), request->content_type); } else { - result = client.Get("..."); + result = client.Get(path, headers); } if (result) { From 08f0a9953d58aa106092ecad66ea357e2c2d76e5 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Wed, 25 Feb 2026 14:19:57 -0700 Subject: [PATCH 12/12] Add license headers --- src/retroachievements/client.cpp | 4 ++++ src/retroachievements/client.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/retroachievements/client.cpp b/src/retroachievements/client.cpp index 43588b996..836006de4 100644 --- a/src/retroachievements/client.cpp +++ b/src/retroachievements/client.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + #include "client.h" #include diff --git a/src/retroachievements/client.h b/src/retroachievements/client.h index c8005b7e6..2ea1799ad 100644 --- a/src/retroachievements/client.h +++ b/src/retroachievements/client.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + #pragma once namespace Core {