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/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 2ee7018a3..07d747c1e 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -519,4 +519,9 @@ 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 +if (ENABLE_RETROACHIEVEMENTS) + add_subdirectory(rcheevos) +endif() 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/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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0da8bbe3..9ef6252a3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -184,6 +184,11 @@ if(ENABLE_DEVELOPER_OPTIONS) add_compile_definitions(ENABLE_DEVELOPER_OPTIONS) endif() +if (ENABLE_RETROACHIEVEMENTS) + add_compile_definitions(ENABLE_RETROACHIEVEMENTS) + add_subdirectory(retroachievements) +endif() + add_subdirectory(common) add_subdirectory(core) add_subdirectory(video_core) diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 20971828a..57ba3efca 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -334,3 +334,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 retroachievements) +endif() diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index ca548b21b..e770ea233 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 "retroachievements/client.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->retroachievements_group->setVisible(false); +#endif + SetupPerGameUI(); SetConfiguration(); @@ -74,6 +82,9 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) } ui->change_screenshot_dir->setEnabled(true); }); + + connect(ui->retroachievements_log_in_button, &QPushButton::clicked, this, + &ConfigureGeneral::RetroAchievementsLogIn); } ConfigureGeneral::~ConfigureGeneral() = default; @@ -196,6 +207,16 @@ void ConfigureGeneral::RetranslateUI() { ui->retranslateUi(this); } +void ConfigureGeneral::RetroAchievementsLogIn() { +#ifdef ENABLE_RETROACHIEVEMENTS + std::string username = ui->retroachievements_username_input->text().toStdString(), + password = ui->retroachievements_password_input->text().toStdString(); + + Core::System::GetInstance().RetroAchievementsClient().LogInUser(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..9468d0c35 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,82 @@ + + + + RetroAchievements + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Username + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Password + + + + + + + QLineEdit::PasswordEchoOnEdit + + + + + + + + + + Log In + + + + + + diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index fe905dfe7..c951a5b1c 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(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 215bdbb3f..9a72cc5a4 100644 --- a/src/common/logging/types.h +++ b/src/common/logging/types.h @@ -52,60 +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 - 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 fd8212dcf..e10318ca9 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -506,6 +506,11 @@ target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network 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 retroachievements) + target_compile_definitions(citra_core PUBLIC ENABLE_RETROACHIEVEMENTS) +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 42cba3160..89aa6b568 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 "retroachievements/client.h" +#endif #include "video_core/custom_textures/custom_tex_manager.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" @@ -73,7 +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 + retroachievements_client = std::make_unique(*this); + retroachievements_client->Initialize(); +#endif +} System::~System() = default; @@ -653,6 +661,16 @@ const Cheats::CheatEngine& System::CheatEngine() const { return cheat_engine; } +#ifdef ENABLE_RETROACHIEVEMENTS +RetroAchievements::Client& System::RetroAchievementsClient() { + return *retroachievements_client; +} + +const RetroAchievements::Client& System::RetroAchievementsClient() const { + return *retroachievements_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 ca61e8262..a3eb793f2 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -70,6 +70,12 @@ namespace Loader { class AppLoader; } +#ifdef ENABLE_RETROACHIEVEMENTS +namespace RetroAchievements { +class Client; +} // namespace RetroAchievements +#endif + namespace Core { class ARM_Interface; @@ -277,6 +283,14 @@ public: /// Gets a const reference to the cheat engine [[nodiscard]] const Cheats::CheatEngine& CheatEngine() const; +#ifdef ENABLE_RETROACHIEVEMENTS + // Gets a reference to the RetroAchievements client + [[nodiscard]] RetroAchievements::Client& RetroAchievementsClient(); + + // Gets a const reference to the RetroAchievements client + [[nodiscard]] const RetroAchievements::Client& RetroAchievementsClient() const; +#endif + /// Gets a reference to the custom texture cache system [[nodiscard]] VideoCore::CustomTexManager& CustomTexManager(); @@ -430,6 +444,11 @@ private: /// Cheats manager Cheats::CheatEngine cheat_engine; +#ifdef ENABLE_RETROACHIEVEMENTS + /// RetroAchievements + std::unique_ptr retroachievements_client; +#endif + /// Video dumper backend std::shared_ptr video_dumper; 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/retroachievements/client.cpp b/src/retroachievements/client.cpp new file mode 100644 index 000000000..836006de4 --- /dev/null +++ b/src/retroachievements/client.cpp @@ -0,0 +1,113 @@ +// 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 +#include + +#include +#include +#include + +#include "common/logging/log.h" +#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". +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; +} + +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."); + + 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? + httplib::Result result; + if (request->post_data) { + result = client.Post(path, headers, request->post_data, std::strlen(request->post_data), + request->content_type); + } else { + result = client.Get(path, headers); + } + + 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()); + } +} + +// 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); +} +} // namespace Callbacks + +Client::Client(const Core::System& _system) : system{_system} {} + +Client::~Client() { + if (rc_client) { + rc_client_destroy(rc_client); + rc_client = NULL; + } +} + +void Client::Initialize() { + 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); +} + +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); + + // 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); +} +} // namespace RetroAchievements diff --git a/src/retroachievements/client.h b/src/retroachievements/client.h new file mode 100644 index 000000000..2ea1799ad --- /dev/null +++ b/src/retroachievements/client.h @@ -0,0 +1,27 @@ +// 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 { +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