diff --git a/Externals/rcheevos/rcheevos b/Externals/rcheevos/rcheevos index c5304a61bc..d9e990e6d1 160000 --- a/Externals/rcheevos/rcheevos +++ b/Externals/rcheevos/rcheevos @@ -1 +1 @@ -Subproject commit c5304a61bcf256ae80fcd1c8f64ad9646aaea757 +Subproject commit d9e990e6d13527532b7e2bb23164a1f3b7f33bb5 diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 7e0fa5a9d9..a566ae6a63 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -4,10 +4,15 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include "Core/AchievementManager.h" + +#include +#include + #include "Common/HttpRequest.h" #include "Common/WorkQueueThread.h" #include "Config/AchievementSettings.h" #include "Core/Core.h" +#include "DiscIO/Volume.h" AchievementManager* AchievementManager::GetInstance() { @@ -28,49 +33,185 @@ void AchievementManager::Init() AchievementManager::ResponseType AchievementManager::Login(const std::string& password) { + if (!m_is_runtime_initialized) + return AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED; return VerifyCredentials(password); } -void AchievementManager::LoginAsync(const std::string& password, const LoginCallback& callback) +void AchievementManager::LoginAsync(const std::string& password, const ResponseCallback& callback) { + if (!m_is_runtime_initialized) + { + callback(AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED); + return; + } m_queue.EmplaceItem([this, password, callback] { callback(VerifyCredentials(password)); }); } bool AchievementManager::IsLoggedIn() const { - return m_login_data.response.succeeded; + return !Config::Get(Config::RA_API_TOKEN).empty(); +} + +void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, + const ResponseCallback& callback) +{ + if (!m_is_runtime_initialized) + { + callback(AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED); + return; + } + struct FilereaderState + { + int64_t position = 0; + std::unique_ptr volume; + }; + rc_hash_filereader volume_reader{ + .open = + [](const char* path_utf8) { + auto state = std::make_unique(); + state->volume = DiscIO::CreateVolume(path_utf8); + return reinterpret_cast(state.release()); + }, + .seek = + [](void* file_handle, int64_t offset, int origin) { + switch (origin) + { + case SEEK_SET: + reinterpret_cast(file_handle)->position = offset; + break; + case SEEK_CUR: + reinterpret_cast(file_handle)->position += offset; + break; + case SEEK_END: + // Unused + break; + } + }, + .tell = + [](void* file_handle) { + return reinterpret_cast(file_handle)->position; + }, + .read = + [](void* file_handle, void* buffer, size_t requested_bytes) { + FilereaderState* filereader_state = reinterpret_cast(file_handle); + bool success = (filereader_state->volume->Read( + filereader_state->position, requested_bytes, reinterpret_cast(buffer), + DiscIO::PARTITION_NONE)); + if (success) + { + filereader_state->position += requested_bytes; + return requested_bytes; + } + else + { + return static_cast(0); + } + }, + .close = [](void* file_handle) { delete reinterpret_cast(file_handle); }}; + rc_hash_init_custom_filereader(&volume_reader); + std::array game_hash; + rc_hash_generate_from_file(game_hash.data(), RC_CONSOLE_GAMECUBE, iso_path.c_str()); + m_queue.EmplaceItem([this, callback, game_hash] { + const auto resolve_hash_response = ResolveHash(game_hash); + if (resolve_hash_response != ResponseType::SUCCESS || m_game_id == 0) + { + callback(resolve_hash_response); + return; + } + + const auto start_session_response = StartRASession(); + if (start_session_response != ResponseType::SUCCESS) + { + callback(start_session_response); + return; + } + + const auto fetch_game_data_response = FetchGameData(); + m_is_game_loaded = fetch_game_data_response == ResponseType::SUCCESS; + callback(fetch_game_data_response); + }); +} + +void AchievementManager::CloseGame() +{ + m_is_game_loaded = false; + m_game_id = 0; + m_queue.Cancel(); } void AchievementManager::Logout() { + CloseGame(); Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); - rc_api_destroy_login_response(&m_login_data); - m_login_data.response.succeeded = 0; } void AchievementManager::Shutdown() { + CloseGame(); m_is_runtime_initialized = false; m_queue.Shutdown(); // DON'T log out - keep those credentials for next run. - rc_api_destroy_login_response(&m_login_data); - m_login_data.response.succeeded = 0; rc_runtime_destroy(&m_runtime); } AchievementManager::ResponseType AchievementManager::VerifyCredentials(const std::string& password) { + rc_api_login_response_t login_data{}; std::string username = Config::Get(Config::RA_USERNAME); std::string api_token = Config::Get(Config::RA_API_TOKEN); rc_api_login_request_t login_request = { .username = username.c_str(), .api_token = api_token.c_str(), .password = password.c_str()}; ResponseType r_type = Request( - login_request, &m_login_data, rc_api_init_login_request, rc_api_process_login_response); + login_request, &login_data, rc_api_init_login_request, rc_api_process_login_response); if (r_type == ResponseType::SUCCESS) - Config::SetBaseOrCurrent(Config::RA_API_TOKEN, m_login_data.api_token); + Config::SetBaseOrCurrent(Config::RA_API_TOKEN, login_data.api_token); + rc_api_destroy_login_response(&login_data); return r_type; } +AchievementManager::ResponseType +AchievementManager::ResolveHash(std::array game_hash) +{ + rc_api_resolve_hash_response_t hash_data{}; + std::string username = Config::Get(Config::RA_USERNAME); + std::string api_token = Config::Get(Config::RA_API_TOKEN); + rc_api_resolve_hash_request_t resolve_hash_request = { + .username = username.c_str(), .api_token = api_token.c_str(), .game_hash = game_hash.data()}; + ResponseType r_type = Request( + resolve_hash_request, &hash_data, rc_api_init_resolve_hash_request, + rc_api_process_resolve_hash_response); + if (r_type == ResponseType::SUCCESS) + m_game_id = hash_data.game_id; + rc_api_destroy_resolve_hash_response(&hash_data); + return r_type; +} + +AchievementManager::ResponseType AchievementManager::StartRASession() +{ + rc_api_start_session_response_t session_data{}; + std::string username = Config::Get(Config::RA_USERNAME); + std::string api_token = Config::Get(Config::RA_API_TOKEN); + rc_api_start_session_request_t start_session_request = { + .username = username.c_str(), .api_token = api_token.c_str(), .game_id = m_game_id}; + ResponseType r_type = Request( + start_session_request, &session_data, rc_api_init_start_session_request, + rc_api_process_start_session_response); + rc_api_destroy_start_session_response(&session_data); + return r_type; +} + +AchievementManager::ResponseType AchievementManager::FetchGameData() +{ + std::string username = Config::Get(Config::RA_USERNAME); + std::string api_token = Config::Get(Config::RA_API_TOKEN); + rc_api_fetch_game_data_request_t fetch_data_request = { + .username = username.c_str(), .api_token = api_token.c_str(), .game_id = m_game_id}; + return Request( + fetch_data_request, &m_game_data, rc_api_init_fetch_game_data_request, + rc_api_process_fetch_game_data_response); +} + // Every RetroAchievements API call, with only a partial exception for fetch_image, follows // the same design pattern (here, X is the name of the call): // Create a specific rc_api_X_request_t struct and populate with the necessary values diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 91fd9339ef..ecbf236d60 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -21,24 +22,33 @@ public: enum class ResponseType { SUCCESS, + MANAGER_NOT_INITIALIZED, INVALID_CREDENTIALS, CONNECTION_FAILED, UNKNOWN_FAILURE }; - using LoginCallback = std::function; + using ResponseCallback = std::function; static AchievementManager* GetInstance(); void Init(); ResponseType Login(const std::string& password); - void LoginAsync(const std::string& password, const LoginCallback& callback); + void LoginAsync(const std::string& password, const ResponseCallback& callback); bool IsLoggedIn() const; + void LoadGameByFilenameAsync(const std::string& iso_path, const ResponseCallback& callback); + + void CloseGame(); void Logout(); void Shutdown(); private: AchievementManager() = default; + static constexpr int HASH_LENGTH = 33; + ResponseType VerifyCredentials(const std::string& password); + ResponseType ResolveHash(std::array game_hash); + ResponseType StartRASession(); + ResponseType FetchGameData(); template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, @@ -47,7 +57,10 @@ private: rc_runtime_t m_runtime{}; bool m_is_runtime_initialized = false; - rc_api_login_response_t m_login_data{}; + unsigned int m_game_id = 0; + rc_api_fetch_game_data_response_t m_game_data{}; + bool m_is_game_loaded = false; + Common::WorkQueueThread> m_queue; }; // class AchievementManager diff --git a/Source/Core/Core/BootManager.cpp b/Source/Core/Core/BootManager.cpp index d2c1483994..1c9e2f03d4 100644 --- a/Source/Core/Core/BootManager.cpp +++ b/Source/Core/Core/BootManager.cpp @@ -29,6 +29,7 @@ #include "Common/IniFile.h" #include "Common/Logging/Log.h" +#include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" #include "Core/Config/MainSettings.h" #include "Core/Config/SYSCONFSettings.h" @@ -163,6 +164,16 @@ bool BootCore(std::unique_ptr boot, const WindowSystemInfo& wsi) } } +#ifdef USE_RETRO_ACHIEVEMENTS + std::string path = ""; + if (std::holds_alternative(boot->parameters)) + { + path = std::get(boot->parameters).path; + } + AchievementManager::GetInstance()->LoadGameByFilenameAsync( + path, [](AchievementManager::ResponseType r_type) {}); +#endif // USE_RETRO_ACHIEVEMENTS + const bool load_ipl = !StartUp.bWii && !Config::Get(Config::MAIN_SKIP_IPL) && std::holds_alternative(boot->parameters); if (load_ipl) diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 4294faaf45..435b26985d 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -38,6 +38,7 @@ #include "Common/Timer.h" #include "Common/Version.h" +#include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" #include "Core/BootManager.h" #include "Core/Config/MainSettings.h" @@ -283,6 +284,10 @@ void Stop() // - Hammertime! if (GetState() == State::Stopping || GetState() == State::Uninitialized) return; +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance()->CloseGame(); +#endif // USE_RETRO_ACHIEVEMENTS + s_is_stopping = true; CallOnStateChangedCallbacks(State::Stopping);