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();