diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 9bb3bcddc2..bf714f0e65 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -5,6 +5,7 @@ #include "Core/AchievementManager.h" +#include #include #include @@ -12,7 +13,6 @@ #include #include -#include "Common/HttpRequest.h" #include "Common/Image.h" #include "Common/Logging/Log.h" #include "Common/WorkQueueThread.h" @@ -34,18 +34,23 @@ AchievementManager& AchievementManager::GetInstance() void AchievementManager::Init() { - if (!m_is_runtime_initialized && Config::Get(Config::RA_ENABLED)) + if (!m_client && Config::Get(Config::RA_ENABLED)) { + m_client = rc_client_create(MemoryPeeker, Request); std::string host_url = Config::Get(Config::RA_HOST_URL); if (!host_url.empty()) - rc_api_set_host(host_url.c_str()); - rc_runtime_init(&m_runtime); - m_is_runtime_initialized = true; + rc_client_set_host(m_client, host_url.c_str()); + rc_client_set_event_handler(m_client, EventHandler); + rc_client_enable_logging(m_client, RC_CLIENT_LOG_LEVEL_VERBOSE, + [](const char* message, const rc_client_t* client) { + INFO_LOG_FMT(ACHIEVEMENTS, "{}", message); + }); + rc_client_set_hardcore_enabled(m_client, Config::Get(Config::RA_HARDCORE_ENABLED)); m_queue.Reset("AchievementManagerQueue", [](const std::function& func) { func(); }); m_image_queue.Reset("AchievementManagerImageQueue", [](const std::function& func) { func(); }); - if (IsLoggedIn()) - LoginAsync("", [](ResponseType r_type) {}); + if (HasAPIToken()) + Login(""); INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager Initialized"); } } @@ -55,614 +60,159 @@ void AchievementManager::SetUpdateCallback(UpdateCallback callback) m_update_callback = std::move(callback); if (!m_update_callback) - m_update_callback = [] {}; + m_update_callback = [](UpdatedItems) {}; - m_update_callback(); + m_update_callback(UpdatedItems{.all = true}); } -AchievementManager::ResponseType AchievementManager::Login(const std::string& password) +void AchievementManager::Login(const std::string& password) { - if (!m_is_runtime_initialized) + if (!m_client) { - ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted login (sync) to RetroAchievements server without " - "Achievement Manager initialized."); - return ResponseType::MANAGER_NOT_INITIALIZED; - } - - const ResponseType r_type = VerifyCredentials(password); - FetchBadges(); - - m_update_callback(); - return r_type; -} - -void AchievementManager::LoginAsync(const std::string& password, const ResponseCallback& callback) -{ - if (!m_is_runtime_initialized) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted login (async) to RetroAchievements server without " - "Achievement Manager initialized."); - callback(ResponseType::MANAGER_NOT_INITIALIZED); + ERROR_LOG_FMT( + ACHIEVEMENTS, + "Attempted login to RetroAchievements server without achievement client initialized."); return; } - m_queue.EmplaceItem([this, password, callback] { - callback(VerifyCredentials(password)); - FetchBadges(); - m_update_callback(); - }); + if (password.empty()) + { + rc_client_begin_login_with_token(m_client, Config::Get(Config::RA_USERNAME).c_str(), + Config::Get(Config::RA_API_TOKEN).c_str(), LoginCallback, + nullptr); + } + else + { + rc_client_begin_login_with_password(m_client, Config::Get(Config::RA_USERNAME).c_str(), + password.c_str(), LoginCallback, nullptr); + } } -bool AchievementManager::IsLoggedIn() const +bool AchievementManager::HasAPIToken() const { return !Config::Get(Config::RA_API_TOKEN).empty(); } -void AchievementManager::HashGame(const std::string& file_path, const ResponseCallback& callback) +void AchievementManager::LoadGame(const std::string& file_path, const DiscIO::Volume* volume) { - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) + if (!Config::Get(Config::RA_ENABLED) || !HasAPIToken()) { - callback(ResponseType::NOT_ENABLED); return; } - if (!m_is_runtime_initialized) + if (file_path.empty() && volume == nullptr) + { + WARN_LOG_FMT(ACHIEVEMENTS, "Called Load Game without a game."); + return; + } + if (!m_client) { ERROR_LOG_FMT(ACHIEVEMENTS, - "Attempted to load game achievements without Achievement Manager initialized."); - callback(ResponseType::MANAGER_NOT_INITIALIZED); + "Attempted to load game achievements without achievement client initialized."); return; } - if (m_disabled) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager is disabled until core is rebooted."); - OSD::AddMessage("Achievements are disabled until you restart emulation.", - OSD::Duration::VERY_LONG, OSD::Color::RED); - return; - } - m_system = &Core::System::GetInstance(); - m_queue.EmplaceItem([this, callback, file_path] { - Hash new_hash; - { - std::lock_guard lg{m_filereader_lock}; - rc_hash_filereader volume_reader{ - .open = &AchievementManager::FilereaderOpenByFilepath, - .seek = &AchievementManager::FilereaderSeek, - .tell = &AchievementManager::FilereaderTell, - .read = &AchievementManager::FilereaderRead, - .close = &AchievementManager::FilereaderClose, - }; - rc_hash_init_custom_filereader(&volume_reader); - if (!rc_hash_generate_from_file(new_hash.data(), RC_CONSOLE_GAMECUBE, file_path.c_str())) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to generate achievement hash from game file {}.", - file_path); - callback(ResponseType::MALFORMED_OBJECT); - } - } - { - std::lock_guard lg{m_lock}; - if (m_disabled) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving."); - callback(ResponseType::EXPIRED_CONTEXT); - return; - } - m_game_hash = std::move(new_hash); - } - LoadGameSync(callback); - }); -} - -void AchievementManager::HashGame(const DiscIO::Volume* volume, const ResponseCallback& callback) -{ - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) - { - callback(ResponseType::NOT_ENABLED); - return; - } - if (!m_is_runtime_initialized) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Attempted to load game achievements without Achievement Manager initialized."); - callback(ResponseType::MANAGER_NOT_INITIALIZED); - return; - } - if (volume == nullptr) - { - INFO_LOG_FMT(ACHIEVEMENTS, "New volume is empty."); - return; - } - if (m_disabled) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager is disabled until core is rebooted."); - OSD::AddMessage("Achievements are disabled until core is rebooted.", OSD::Duration::VERY_LONG, - OSD::Color::RED); - return; - } - // Need to SetDisabled outside a lock because it uses m_lock internally. - bool disable = true; + rc_client_set_unofficial_enabled(m_client, Config::Get(Config::RA_UNOFFICIAL_ENABLED)); + rc_client_set_encore_mode_enabled(m_client, Config::Get(Config::RA_ENCORE_ENABLED)); + rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED)); + if (volume) { std::lock_guard lg{m_lock}; if (!m_loading_volume) { m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader()); - disable = false; } } - if (disable) + std::lock_guard lg{m_filereader_lock}; + rc_hash_filereader volume_reader{ + .open = (volume) ? &AchievementManager::FilereaderOpenByVolume : + &AchievementManager::FilereaderOpenByFilepath, + .seek = &AchievementManager::FilereaderSeek, + .tell = &AchievementManager::FilereaderTell, + .read = &AchievementManager::FilereaderRead, + .close = &AchievementManager::FilereaderClose, + }; + rc_hash_init_custom_filereader(&volume_reader); + if (rc_client_get_game_info(m_client)) { - INFO_LOG_FMT(ACHIEVEMENTS, "Disabling Achievement Manager due to hash spam."); - SetDisabled(true); - callback(ResponseType::EXPIRED_CONTEXT); - return; + rc_client_begin_change_media(m_client, file_path.c_str(), NULL, 0, ChangeMediaCallback, NULL); } - m_system = &Core::System::GetInstance(); - m_queue.EmplaceItem([this, callback] { - Hash new_hash; - { - std::lock_guard lg{m_filereader_lock}; - rc_hash_filereader volume_reader{ - .open = &AchievementManager::FilereaderOpenByVolume, - .seek = &AchievementManager::FilereaderSeek, - .tell = &AchievementManager::FilereaderTell, - .read = &AchievementManager::FilereaderRead, - .close = &AchievementManager::FilereaderClose, - }; - rc_hash_init_custom_filereader(&volume_reader); - if (!rc_hash_generate_from_file(new_hash.data(), RC_CONSOLE_GAMECUBE, "")) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to generate achievement hash from volume."); - callback(ResponseType::MALFORMED_OBJECT); - return; - } - } - { - std::lock_guard lg{m_lock}; - if (m_disabled) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving."); - callback(ResponseType::EXPIRED_CONTEXT); - return; - } - m_game_hash = std::move(new_hash); - m_loading_volume.reset(); - } - LoadGameSync(callback); - }); -} - -void AchievementManager::LoadGameSync(const ResponseCallback& callback) -{ - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) + else { - callback(ResponseType::NOT_ENABLED); - return; + rc_client_begin_identify_and_load_game(m_client, RC_CONSOLE_GAMECUBE, file_path.c_str(), NULL, + 0, LoadGameCallback, NULL); } - u32 new_game_id = 0; - Hash current_hash; - { - std::lock_guard lg{m_lock}; - current_hash = m_game_hash; - } - const auto resolve_hash_response = ResolveHash(current_hash, &new_game_id); - if (resolve_hash_response != ResponseType::SUCCESS || new_game_id == 0) - { - INFO_LOG_FMT(ACHIEVEMENTS, "No RetroAchievements data found for this game."); - OSD::AddMessage("No RetroAchievements data found for this game.", OSD::Duration::VERY_LONG, - OSD::Color::RED); - SetDisabled(true); - callback(resolve_hash_response); - return; - } - u32 old_game_id; - { - std::lock_guard lg{m_lock}; - old_game_id = m_game_id; - } - if (new_game_id == old_game_id) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Alternate hash resolved for current game {}.", old_game_id); - callback(ResponseType::SUCCESS); - return; - } - else if (old_game_id != 0) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Swapping game {} for game {}; achievements disabled.", old_game_id, - new_game_id); - OSD::AddMessage("Achievements are now disabled. Please close emulation to re-enable.", - OSD::Duration::VERY_LONG, OSD::Color::RED); - SetDisabled(true); - callback(ResponseType::EXPIRED_CONTEXT); - return; - } - { - std::lock_guard lg{m_lock}; - m_game_id = new_game_id; - } - - const auto start_session_response = StartRASession(); - if (start_session_response != ResponseType::SUCCESS) - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to connect to RetroAchievements server."); - OSD::AddMessage("Failed to connect to RetroAchievements server.", OSD::Duration::VERY_LONG, - OSD::Color::RED); - callback(start_session_response); - return; - } - - const auto fetch_game_data_response = FetchGameData(); - if (fetch_game_data_response != ResponseType::SUCCESS) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Unable to retrieve data from RetroAchievements server."); - OSD::AddMessage("Unable to retrieve data from RetroAchievements server.", - OSD::Duration::VERY_LONG, OSD::Color::RED); - return; - } - INFO_LOG_FMT(ACHIEVEMENTS, "Loading achievements for {}.", m_game_data.title); - - // Claim the lock, then queue the fetch unlock data calls, then initialize the unlock map in - // ActivateDeactiveAchievements. This allows the calls to process while initializing the - // unlock map but then forces them to wait until it's initialized before making modifications to - // it. - { - std::lock_guard lg{m_lock}; - m_is_game_loaded = true; - m_framecount = 0; - LoadUnlockData([](ResponseType r_type) {}); - ActivateDeactivateAchievements(); - ActivateDeactivateLeaderboards(); - ActivateDeactivateRichPresence(); - } - FetchBadges(); - // Reset this to zero so that RP immediately triggers on the first frame - m_last_ping_time = 0; - INFO_LOG_FMT(ACHIEVEMENTS, "RetroAchievements successfully loaded for {}.", m_game_data.title); - - m_update_callback(); - callback(fetch_game_data_response); } bool AchievementManager::IsGameLoaded() const { - return m_is_game_loaded; + auto* game_info = rc_client_get_game_info(m_client); + return game_info && game_info->id != 0; } -void AchievementManager::LoadUnlockData(const ResponseCallback& callback) +void AchievementManager::FetchPlayerBadge() { - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) - { - callback(ResponseType::NOT_ENABLED); - return; - } - m_queue.EmplaceItem([this, callback] { - const auto hardcore_unlock_response = FetchUnlockData(true); - if (hardcore_unlock_response != ResponseType::SUCCESS) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Failed to fetch hardcore unlock data; skipping softcore unlock."); - callback(hardcore_unlock_response); - return; - } - - callback(FetchUnlockData(false)); - m_update_callback(); - }); + FetchBadge(&m_player_badge, RC_IMAGE_TYPE_USER, + [](const AchievementManager& manager) { + auto* user_info = rc_client_get_user_info(manager.m_client); + if (!user_info) + return std::string(""); + return std::string(user_info->display_name); + }, + {.player_icon = true}); } -void AchievementManager::ActivateDeactivateAchievements() +void AchievementManager::FetchGameBadges() { - std::lock_guard lg{m_lock}; - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) + FetchBadge(&m_game_badge, RC_IMAGE_TYPE_GAME, + [](const AchievementManager& manager) { + auto* game_info = rc_client_get_game_info(manager.m_client); + if (!game_info) + return std::string(""); + return std::string(game_info->badge_name); + }, + {.game_icon = true}); + + if (!rc_client_has_achievements(m_client)) return; - bool enabled = Config::Get(Config::RA_ACHIEVEMENTS_ENABLED); - bool unofficial = Config::Get(Config::RA_UNOFFICIAL_ENABLED); - bool encore = Config::Get(Config::RA_ENCORE_ENABLED); - for (u32 ix = 0; ix < m_game_data.num_achievements; ix++) - { - auto iter = - m_unlock_map.insert({m_game_data.achievements[ix].id, - UnlockStatus{.game_data_index = ix, - .points = m_game_data.achievements[ix].points, - .category = m_game_data.achievements[ix].category}}); - ActivateDeactivateAchievement(iter.first->first, enabled, unofficial, encore); - } - INFO_LOG_FMT(ACHIEVEMENTS, "Achievements (de)activated."); -} -void AchievementManager::ActivateDeactivateLeaderboards() -{ - std::lock_guard lg{m_lock}; - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) - return; - bool leaderboards_enabled = - Config::Get(Config::RA_LEADERBOARDS_ENABLED) && Config::Get(Config::RA_HARDCORE_ENABLED); - for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++) - { - auto leaderboard = m_game_data.leaderboards[ix]; - u32 leaderboard_id = leaderboard.id; - if (m_is_game_loaded && leaderboards_enabled) - { - rc_runtime_activate_lboard(&m_runtime, leaderboard_id, leaderboard.definition, nullptr, 0); - m_queue.EmplaceItem([this, leaderboard_id] { - FetchBoardInfo(leaderboard_id); - m_update_callback(); - }); - } - else - { - rc_runtime_deactivate_lboard(&m_runtime, m_game_data.leaderboards[ix].id); - } - } - INFO_LOG_FMT(ACHIEVEMENTS, "Leaderboards (de)activated."); -} - -void AchievementManager::ActivateDeactivateRichPresence() -{ - std::lock_guard lg{m_lock}; - if (!Config::Get(Config::RA_ENABLED) || !IsLoggedIn()) - return; - rc_runtime_activate_richpresence( - &m_runtime, - (m_is_game_loaded && Config::Get(Config::RA_RICH_PRESENCE_ENABLED)) ? - m_game_data.rich_presence_script : - "", - nullptr, 0); - INFO_LOG_FMT(ACHIEVEMENTS, "Rich presence (de)activated."); -} - -void AchievementManager::FetchBadges() -{ - if (!m_is_runtime_initialized || !IsLoggedIn() || !Config::Get(Config::RA_BADGES_ENABLED)) - { - m_update_callback(); - return; - } - m_image_queue.Cancel(); - - if (m_player_badge.name != m_display_name) - { - m_image_queue.EmplaceItem([this] { - std::string name_to_fetch; - { - std::lock_guard lg{m_lock}; - if (m_display_name == m_player_badge.name) - return; - name_to_fetch = m_display_name; - } - rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(), - .image_type = RC_IMAGE_TYPE_USER}; - Badge fetched_badge; - if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded player badge id {}.", name_to_fetch); - std::lock_guard lg{m_lock}; - if (name_to_fetch != m_display_name) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Requested outdated badge id {} for player id {}.", - name_to_fetch, m_display_name); - return; - } - m_player_badge.badge = std::move(fetched_badge); - m_player_badge.name = std::move(name_to_fetch); - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download player badge id {}.", name_to_fetch); - } - - m_update_callback(); - }); - } - - if (!IsGameLoaded()) - { - m_update_callback(); - return; - } - - bool badgematch = false; + rc_client_achievement_list_t* achievement_list; { std::lock_guard lg{m_lock}; - badgematch = m_game_badge.name == m_game_data.image_name; + achievement_list = rc_client_create_achievement_list( + m_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); } - if (!badgematch) + for (u32 bx = 0; bx < achievement_list->num_buckets; bx++) { - m_image_queue.EmplaceItem([this] { - std::string name_to_fetch; - { - std::lock_guard lg{m_lock}; - if (m_game_badge.name == m_game_data.image_name) - return; - name_to_fetch = m_game_data.image_name; - } - rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(), - .image_type = RC_IMAGE_TYPE_GAME}; - Badge fetched_badge; - if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded game badge id {}.", name_to_fetch); - std::lock_guard lg{m_lock}; - if (name_to_fetch != m_game_data.image_name) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Requested outdated badge id {} for game id {}.", - name_to_fetch, m_game_data.image_name); - return; - } - m_game_badge.badge = std::move(fetched_badge); - m_game_badge.name = std::move(name_to_fetch); - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download game badge id {}.", name_to_fetch); - } - - m_update_callback(); - }); - } - - unsigned num_achievements = m_game_data.num_achievements; - for (size_t index = 0; index < num_achievements; index++) - { - std::lock_guard lg{m_lock}; - - // In case the number of achievements changes since the loop started; I just don't want - // to lock for the ENTIRE loop so instead I reclaim the lock each cycle - if (num_achievements != m_game_data.num_achievements) - break; - - const auto& initial_achievement = m_game_data.achievements[index]; - const std::string badge_name_to_fetch(initial_achievement.badge_name); - const UnlockStatus& unlock_status = m_unlock_map[initial_achievement.id]; - - if (unlock_status.unlocked_badge.name != badge_name_to_fetch) + auto& bucket = achievement_list->buckets[bx]; + for (u32 achievement = 0; achievement < bucket.num_achievements; achievement++) { - m_image_queue.EmplaceItem([this, index] { - std::string current_name, name_to_fetch; - { - std::lock_guard lock{m_lock}; - if (m_game_data.num_achievements <= index) - { - INFO_LOG_FMT( - ACHIEVEMENTS, - "Attempted to fetch unlocked badge for index {} after achievement list cleared.", - index); - return; - } - const auto& achievement = m_game_data.achievements[index]; - const auto unlock_itr = m_unlock_map.find(achievement.id); - if (unlock_itr == m_unlock_map.end()) - { - ERROR_LOG_FMT( - ACHIEVEMENTS, - "Attempted to fetch unlocked badge for achievement id {} not in unlock map.", - index); - return; - } - name_to_fetch = achievement.badge_name; - current_name = unlock_itr->second.unlocked_badge.name; - } - if (current_name == name_to_fetch) - return; - rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(), - .image_type = RC_IMAGE_TYPE_ACHIEVEMENT}; - Badge fetched_badge; - if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded unlocked achievement badge id {}.", - name_to_fetch); - std::lock_guard lock{m_lock}; - if (m_game_data.num_achievements <= index) - { - INFO_LOG_FMT(ACHIEVEMENTS, - "Fetched unlocked badge for index {} after achievement list cleared.", - index); - return; - } - const auto& achievement = m_game_data.achievements[index]; - const auto unlock_itr = m_unlock_map.find(achievement.id); - if (unlock_itr == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Fetched unlocked badge for achievement id {} not in unlock map.", index); - return; - } - if (name_to_fetch != achievement.badge_name) - { - INFO_LOG_FMT( - ACHIEVEMENTS, - "Requested outdated unlocked achievement badge id {} for achievement id {}.", - name_to_fetch, current_name); - return; - } - unlock_itr->second.unlocked_badge.badge = std::move(fetched_badge); - unlock_itr->second.unlocked_badge.name = std::move(name_to_fetch); - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download unlocked achievement badge id {}.", - name_to_fetch); - } + u32 achievement_id = bucket.achievements[achievement]->id; - m_update_callback(); - }); - } - if (unlock_status.locked_badge.name != badge_name_to_fetch) - { - m_image_queue.EmplaceItem([this, index] { - std::string current_name, name_to_fetch; - { - std::lock_guard lock{m_lock}; - if (m_game_data.num_achievements <= index) - { - INFO_LOG_FMT( - ACHIEVEMENTS, - "Attempted to fetch locked badge for index {} after achievement list cleared.", - index); - return; - } - const auto& achievement = m_game_data.achievements[index]; - const auto unlock_itr = m_unlock_map.find(achievement.id); - if (unlock_itr == m_unlock_map.end()) - { - ERROR_LOG_FMT( - ACHIEVEMENTS, - "Attempted to fetch locked badge for achievement id {} not in unlock map.", index); - return; - } - name_to_fetch = achievement.badge_name; - current_name = unlock_itr->second.locked_badge.name; - } - if (current_name == name_to_fetch) - return; - rc_api_fetch_image_request_t icon_request = { - .image_name = name_to_fetch.c_str(), .image_type = RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED}; - Badge fetched_badge; - if (RequestImage(icon_request, &fetched_badge) == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded locked achievement badge id {}.", - name_to_fetch); - std::lock_guard lock{m_lock}; - if (m_game_data.num_achievements <= index) - { - INFO_LOG_FMT(ACHIEVEMENTS, - "Fetched locked badge for index {} after achievement list cleared.", - index); - return; - } - const auto& achievement = m_game_data.achievements[index]; - const auto unlock_itr = m_unlock_map.find(achievement.id); - if (unlock_itr == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Fetched locked badge for achievement id {} not in unlock map.", index); - return; - } - if (name_to_fetch != achievement.badge_name) - { - INFO_LOG_FMT(ACHIEVEMENTS, - "Requested outdated locked achievement badge id {} for achievement id {}.", - name_to_fetch, current_name); - return; - } - unlock_itr->second.locked_badge.badge = std::move(fetched_badge); - unlock_itr->second.locked_badge.name = std::move(name_to_fetch); - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to download locked achievement badge id {}.", - name_to_fetch); - } - - m_update_callback(); - }); + FetchBadge( + &m_unlocked_badges[achievement_id], RC_IMAGE_TYPE_ACHIEVEMENT, + [achievement_id](const AchievementManager& manager) { + if (!rc_client_get_achievement_info(manager.m_client, achievement_id)) + return std::string(""); + return std::string( + rc_client_get_achievement_info(manager.m_client, achievement_id)->badge_name); + }, + {.achievements = {achievement_id}}); + FetchBadge( + &m_locked_badges[achievement_id], RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, + [achievement_id](const AchievementManager& manager) { + if (!rc_client_get_achievement_info(manager.m_client, achievement_id)) + return std::string(""); + return std::string( + rc_client_get_achievement_info(manager.m_client, achievement_id)->badge_name); + }, + {.achievements = {achievement_id}}); } } - - m_update_callback(); + rc_client_destroy_achievement_list(achievement_list); } void AchievementManager::DoFrame() { - if (!m_is_game_loaded || !Core::IsCPUThread()) + if (!IsGameLoaded() || !Core::IsCPUThread()) return; if (m_framecount == 0x200) { @@ -674,83 +224,16 @@ void AchievementManager::DoFrame() } { std::lock_guard lg{m_lock}; - rc_runtime_do_frame( - &m_runtime, - [](const rc_runtime_event_t* runtime_event) { - GetInstance().AchievementEventHandler(runtime_event); - }, - [](unsigned address, unsigned num_bytes, void* ud) { - return static_cast(ud)->MemoryPeeker(address, num_bytes, ud); - }, - this, nullptr); + rc_client_do_frame(m_client); } if (!m_system) return; - time_t current_time = std::time(nullptr); - if (difftime(current_time, m_last_ping_time) > 120) + auto current_time = std::chrono::steady_clock::now(); + if (current_time - m_last_rp_time > std::chrono::seconds{10}) { - GenerateRichPresence(Core::CPUThreadGuard{*m_system}); - m_queue.EmplaceItem([this] { PingRichPresence(m_rich_presence); }); - m_last_ping_time = current_time; - m_update_callback(); - } -} - -u32 AchievementManager::MemoryPeeker(u32 address, u32 num_bytes, void* ud) -{ - if (!m_system) - return 0u; - Core::CPUThreadGuard threadguard(*m_system); - switch (num_bytes) - { - case 1: - return m_system->GetMMU() - .HostTryReadU8(threadguard, address, PowerPC::RequestedAddressSpace::Physical) - .value_or(PowerPC::ReadResult(false, 0u)) - .value; - case 2: - return Common::swap16( - m_system->GetMMU() - .HostTryReadU16(threadguard, address, PowerPC::RequestedAddressSpace::Physical) - .value_or(PowerPC::ReadResult(false, 0u)) - .value); - case 4: - return Common::swap32( - m_system->GetMMU() - .HostTryReadU32(threadguard, address, PowerPC::RequestedAddressSpace::Physical) - .value_or(PowerPC::ReadResult(false, 0u)) - .value); - default: - ASSERT(false); - return 0u; - } -} - -void AchievementManager::AchievementEventHandler(const rc_runtime_event_t* runtime_event) -{ - switch (runtime_event->type) - { - case RC_RUNTIME_EVENT_ACHIEVEMENT_TRIGGERED: - HandleAchievementTriggeredEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_PROGRESS_UPDATED: - HandleAchievementProgressUpdatedEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_PRIMED: - HandleAchievementPrimedEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_ACHIEVEMENT_UNPRIMED: - HandleAchievementUnprimedEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_STARTED: - HandleLeaderboardStartedEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_CANCELED: - HandleLeaderboardCanceledEvent(runtime_event); - break; - case RC_RUNTIME_EVENT_LBOARD_TRIGGERED: - HandleLeaderboardTriggeredEvent(runtime_event); - break; + m_last_rp_time = current_time; + rc_client_get_rich_presence_message(m_client, m_rich_presence.data(), RP_SIZE); + m_update_callback(UpdatedItems{.rich_presence = true}); } } @@ -759,26 +242,44 @@ std::recursive_mutex& AchievementManager::GetLock() return m_lock; } +void AchievementManager::SetHardcoreMode() +{ + rc_client_set_hardcore_enabled(m_client, Config::Get(Config::RA_HARDCORE_ENABLED)); +} + bool AchievementManager::IsHardcoreModeActive() const { std::lock_guard lg{m_lock}; - if (!Config::Get(Config::RA_HARDCORE_ENABLED)) + if (!rc_client_get_hardcore_enabled(m_client)) return false; - if (!Core::IsRunning()) + if (!rc_client_get_game_info(m_client)) return true; - if (!IsGameLoaded()) - return false; - return (m_runtime.trigger_count + m_runtime.lboard_count > 0); + return rc_client_is_processing_required(m_client); } -std::string AchievementManager::GetPlayerDisplayName() const +void AchievementManager::SetSpectatorMode() { - return IsLoggedIn() ? m_display_name : ""; + rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED)); +} + +std::string_view AchievementManager::GetPlayerDisplayName() const +{ + if (!HasAPIToken()) + return ""; + auto* user = rc_client_get_user_info(m_client); + if (!user) + return ""; + return std::string_view(user->display_name); } u32 AchievementManager::GetPlayerScore() const { - return IsLoggedIn() ? m_player_score : 0; + if (!HasAPIToken()) + return 0; + auto* user = rc_client_get_user_info(m_client); + if (!user) + return 0; + return user->score; } const AchievementManager::BadgeStatus& AchievementManager::GetPlayerBadge() const @@ -786,38 +287,14 @@ const AchievementManager::BadgeStatus& AchievementManager::GetPlayerBadge() cons return m_player_badge; } -std::string AchievementManager::GetGameDisplayName() const +std::string_view AchievementManager::GetGameDisplayName() const { - return IsGameLoaded() ? m_game_data.title : ""; + return IsGameLoaded() ? std::string_view(rc_client_get_game_info(m_client)->title) : ""; } -AchievementManager::PointSpread AchievementManager::TallyScore() const +rc_client_t* AchievementManager::GetClient() { - PointSpread spread{}; - if (!IsGameLoaded()) - return spread; - bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED); - for (const auto& entry : m_unlock_map) - { - if (entry.second.category != RC_ACHIEVEMENT_CATEGORY_CORE) - continue; - u32 points = entry.second.points; - spread.total_count++; - spread.total_points += points; - if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::HARDCORE || - (hardcore_mode_enabled && entry.second.session_unlock_count > 0)) - { - spread.hard_unlocks++; - spread.hard_points += points; - } - else if (entry.second.remote_unlock_status == UnlockStatus::UnlockType::SOFTCORE || - entry.second.session_unlock_count > 0) - { - spread.soft_unlocks++; - spread.soft_points += points; - } - } - return spread; + return m_client; } rc_api_fetch_game_data_response_t* AchievementManager::GetGameData() @@ -830,100 +307,110 @@ const AchievementManager::BadgeStatus& AchievementManager::GetGameBadge() const return m_game_badge; } -const AchievementManager::UnlockStatus& -AchievementManager::GetUnlockStatus(AchievementId achievement_id) const +const AchievementManager::BadgeStatus& AchievementManager::GetAchievementBadge(AchievementId id, + bool locked) const { - return m_unlock_map.at(achievement_id); + auto& badge_list = locked ? m_locked_badges : m_locked_badges; + auto itr = badge_list.find(id); + return (itr == badge_list.end()) ? m_default_badge : itr->second; } -AchievementManager::ResponseType -AchievementManager::GetAchievementProgress(AchievementId achievement_id, u32* value, u32* target) +const AchievementManager::LeaderboardStatus* +AchievementManager::GetLeaderboardInfo(AchievementManager::AchievementId leaderboard_id) { - if (!IsGameLoaded()) + if (const auto leaderboard_iter = m_leaderboard_map.find(leaderboard_id); + leaderboard_iter != m_leaderboard_map.end()) { - ERROR_LOG_FMT( - ACHIEVEMENTS, - "Attempted to request measured data for achievement ID {} when no game is running.", - achievement_id); - return ResponseType::INVALID_REQUEST; + if (leaderboard_iter->second.entries.size() == 0) + FetchBoardInfo(leaderboard_id); + return &leaderboard_iter->second; } - int result = rc_runtime_get_achievement_measured(&m_runtime, achievement_id, value, target); - if (result == 0) - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to get measured data for achievement ID {}.", - achievement_id); - return ResponseType::MALFORMED_OBJECT; - } - return ResponseType::SUCCESS; -} -const std::unordered_map& -AchievementManager::GetLeaderboardsInfo() const -{ - return m_leaderboard_map; + return nullptr; } AchievementManager::RichPresence AchievementManager::GetRichPresence() const { - std::lock_guard lg{m_lock}; return m_rich_presence; } -void AchievementManager::SetDisabled(bool disable) -{ - bool previously_disabled; - { - std::lock_guard lg{m_lock}; - previously_disabled = m_disabled; - m_disabled = disable; - if (disable && m_is_game_loaded) - CloseGame(); - } - - if (!previously_disabled && disable && Config::Get(Config::RA_ENABLED)) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been disabled."); - OSD::AddMessage("Please close all games to re-enable achievements.", OSD::Duration::VERY_LONG, - OSD::Color::RED); - m_update_callback(); - } - - if (previously_disabled && !disable) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been re-enabled."); - m_update_callback(); - } -}; - const AchievementManager::NamedIconMap& AchievementManager::GetChallengeIcons() const { return m_active_challenges; } +std::vector AchievementManager::GetActiveLeaderboards() const +{ + std::vector display_values; + for (u32 ix = 0; ix < MAX_DISPLAYED_LBOARDS && ix < m_active_leaderboards.size(); ix++) + { + display_values.push_back(std::string(m_active_leaderboards[ix].display)); + } + return display_values; +} + +void AchievementManager::DoState(PointerWrap& p) +{ + if (!m_client || !Config::Get(Config::RA_ENABLED)) + return; + size_t size = 0; + if (!p.IsReadMode()) + size = rc_client_progress_size(m_client); + p.Do(size); + auto buffer = std::make_unique(size); + if (!p.IsReadMode()) + { + int result = rc_client_serialize_progress_sized(m_client, buffer.get(), size); + if (result != RC_OK) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Failed serializing achievement client with error code {}", + result); + return; + } + } + p.DoArray(buffer.get(), (u32)size); + if (p.IsReadMode()) + { + int result = rc_client_deserialize_progress_sized(m_client, buffer.get(), size); + if (result != RC_OK) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Failed deserializing achievement client with error code {}", + result); + return; + } + size_t new_size = rc_client_progress_size(m_client); + if (size != new_size) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Loaded client size {} does not match size in state {}", new_size, + size); + return; + } + } + p.DoMarker("AchievementManager"); +} + void AchievementManager::CloseGame() { { std::lock_guard lg{m_lock}; - if (m_is_game_loaded) + if (rc_client_get_game_info(m_client)) { - m_is_game_loaded = false; m_active_challenges.clear(); - ActivateDeactivateAchievements(); - ActivateDeactivateLeaderboards(); - ActivateDeactivateRichPresence(); - m_game_id = 0; + m_active_leaderboards.clear(); m_game_badge.name.clear(); - m_unlock_map.clear(); + m_unlocked_badges.clear(); + m_locked_badges.clear(); m_leaderboard_map.clear(); rc_api_destroy_fetch_game_data_response(&m_game_data); m_game_data = {}; m_queue.Cancel(); m_image_queue.Cancel(); + rc_client_unload_game(m_client); m_system = nullptr; } } - m_update_callback(); + m_update_callback(UpdatedItems{.all = true}); INFO_LOG_FMT(ACHIEVEMENTS, "Game closed."); } @@ -932,24 +419,25 @@ void AchievementManager::Logout() { std::lock_guard lg{m_lock}; CloseGame(); - SetDisabled(false); m_player_badge.name.clear(); Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); } - m_update_callback(); + m_update_callback(UpdatedItems{.all = true}); INFO_LOG_FMT(ACHIEVEMENTS, "Logged out from server."); } void AchievementManager::Shutdown() { - CloseGame(); - SetDisabled(false); - m_is_runtime_initialized = false; - m_queue.Shutdown(); - // DON'T log out - keep those credentials for next run. - rc_runtime_destroy(&m_runtime); - INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager shut down."); + if (m_client) + { + CloseGame(); + m_queue.Shutdown(); + // DON'T log out - keep those credentials for next run. + rc_client_destroy(m_client); + m_client = nullptr; + INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager shut down."); + } } void* AchievementManager::FilereaderOpenByFilepath(const char* path_utf8) @@ -1016,680 +504,298 @@ void AchievementManager::FilereaderClose(void* file_handle) delete static_cast(file_handle); } -AchievementManager::ResponseType AchievementManager::VerifyCredentials(const std::string& password) +void AchievementManager::LoginCallback(int result, const char* error_message, rc_client_t* client, + void* userdata) { - rc_api_login_response_t login_data{}; - std::string username, api_token; + if (result != RC_OK) { - std::lock_guard lg{m_lock}; - username = Config::Get(Config::RA_USERNAME); - 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, &login_data, rc_api_init_login_request, rc_api_process_login_response); - if (r_type == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Successfully logged in {} to RetroAchievements server.", username); - std::lock_guard lg{m_lock}; - if (username != Config::Get(Config::RA_USERNAME)) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Attempted to login prior user {}; current user is {}.", username, - Config::Get(Config::RA_USERNAME)); - Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); - return ResponseType::EXPIRED_CONTEXT; - } - Config::SetBaseOrCurrent(Config::RA_API_TOKEN, login_data.api_token); - m_display_name = login_data.display_name; - m_player_score = login_data.score; - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to login {} to RetroAchievements server.", username); - } - rc_api_destroy_login_response(&login_data); - return r_type; -} - -AchievementManager::ResponseType AchievementManager::ResolveHash(const Hash& game_hash, - u32* game_id) -{ - rc_api_resolve_hash_response_t hash_data{}; - std::string username, api_token; - { - std::lock_guard lg{m_lock}; - username = Config::Get(Config::RA_USERNAME); - 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) - { - *game_id = hash_data.game_id; - INFO_LOG_FMT(ACHIEVEMENTS, "Hashed game ID {} for RetroAchievements.", *game_id); - } - else - { - INFO_LOG_FMT(ACHIEVEMENTS, "Hash {} not recognized by RetroAchievements.", game_hash.data()); - } - rc_api_destroy_resolve_hash_response(&hash_data); - return r_type; -} - -AchievementManager::ResponseType AchievementManager::StartRASession() -{ - rc_api_start_session_request_t start_session_request; - rc_api_start_session_response_t session_data{}; - std::string username, api_token; - { - std::lock_guard lg{m_lock}; - username = Config::Get(Config::RA_USERNAME); - api_token = Config::Get(Config::RA_API_TOKEN); - 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() -{ - rc_api_fetch_game_data_request_t fetch_data_request; - rc_api_request_t api_request; - Common::HttpRequest http_request; - std::string username, api_token; - u32 game_id; - { - std::lock_guard lg{m_lock}; - username = Config::Get(Config::RA_USERNAME); - api_token = Config::Get(Config::RA_API_TOKEN); - game_id = m_game_id; - } - fetch_data_request = { - .username = username.c_str(), .api_token = api_token.c_str(), .game_id = game_id}; - if (rc_api_init_fetch_game_data_request(&api_request, &fetch_data_request) != RC_OK || - !api_request.post_data) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid API request for game data."); - return ResponseType::INVALID_REQUEST; - } - auto http_response = http_request.Post(api_request.url, api_request.post_data); - rc_api_destroy_request(&api_request); - if (!http_response.has_value() || http_response->size() == 0) - { - WARN_LOG_FMT(ACHIEVEMENTS, - "RetroAchievements connection failed while fetching game data for ID {}. \nURL: " - "{} \npost_data: {}", - game_id, api_request.url, - api_request.post_data == nullptr ? "NULL" : api_request.post_data); - return ResponseType::CONNECTION_FAILED; - } - std::lock_guard lg{m_lock}; - const std::string response_str(http_response->begin(), http_response->end()); - if (rc_api_process_fetch_game_data_response(&m_game_data, response_str.c_str()) != RC_OK) - { - ERROR_LOG_FMT(ACHIEVEMENTS, - "Failed to process HTTP response fetching game data for ID {}. \nURL: {} " - "\npost_data: {} \nresponse: {}", - game_id, api_request.url, - api_request.post_data == nullptr ? "NULL" : api_request.post_data, response_str); - rc_api_destroy_fetch_game_data_response(&m_game_data); - m_game_data = {}; - return ResponseType::MALFORMED_OBJECT; - } - if (!m_game_data.response.succeeded) - { - WARN_LOG_FMT( - ACHIEVEMENTS, - "Invalid RetroAchievements credentials fetching game data for ID {}; logging out user {}", - game_id, username); - // Logout technically does this via a CloseGame call, but doing this now prevents the activate - // methods from thinking they have something to do. - rc_api_destroy_fetch_game_data_response(&m_game_data); - m_game_data = {}; - Logout(); - return ResponseType::INVALID_CREDENTIALS; - } - if (game_id != m_game_id) - { - INFO_LOG_FMT(ACHIEVEMENTS, - "Attempted to retrieve game data for ID {}; running game is now ID {}", game_id, - m_game_id); - rc_api_destroy_fetch_game_data_response(&m_game_data); - m_game_data = {}; - return ResponseType::EXPIRED_CONTEXT; - } - INFO_LOG_FMT(ACHIEVEMENTS, "Retrieved game data for ID {}.", game_id); - return ResponseType::SUCCESS; -} - -AchievementManager::ResponseType AchievementManager::FetchUnlockData(bool hardcore) -{ - rc_api_fetch_user_unlocks_response_t unlock_data{}; - std::string username = Config::Get(Config::RA_USERNAME); - std::string api_token = Config::Get(Config::RA_API_TOKEN); - rc_api_fetch_user_unlocks_request_t fetch_unlocks_request = {.username = username.c_str(), - .api_token = api_token.c_str(), - .game_id = m_game_id, - .hardcore = hardcore}; - ResponseType r_type = - Request( - fetch_unlocks_request, &unlock_data, rc_api_init_fetch_user_unlocks_request, - rc_api_process_fetch_user_unlocks_response); - if (r_type == ResponseType::SUCCESS) - { - std::lock_guard lg{m_lock}; - bool enabled = Config::Get(Config::RA_ACHIEVEMENTS_ENABLED); - bool unofficial = Config::Get(Config::RA_UNOFFICIAL_ENABLED); - bool encore = Config::Get(Config::RA_ENCORE_ENABLED); - for (AchievementId ix = 0; ix < unlock_data.num_achievement_ids; ix++) - { - auto it = m_unlock_map.find(unlock_data.achievement_ids[ix]); - if (it == m_unlock_map.end()) - continue; - it->second.remote_unlock_status = - hardcore ? UnlockStatus::UnlockType::HARDCORE : UnlockStatus::UnlockType::SOFTCORE; - ActivateDeactivateAchievement(unlock_data.achievement_ids[ix], enabled, unofficial, encore); - } - } - rc_api_destroy_fetch_user_unlocks_response(&unlock_data); - return r_type; -} - -AchievementManager::ResponseType AchievementManager::FetchBoardInfo(AchievementId leaderboard_id) -{ - std::string username = Config::Get(Config::RA_USERNAME); - LeaderboardStatus lboard{}; - - { - rc_api_fetch_leaderboard_info_response_t board_info{}; - const rc_api_fetch_leaderboard_info_request_t fetch_board_request = { - .leaderboard_id = leaderboard_id, .count = 4, .first_entry = 1, .username = nullptr}; - const ResponseType r_type = - Request( - fetch_board_request, &board_info, rc_api_init_fetch_leaderboard_info_request, - rc_api_process_fetch_leaderboard_info_response); - if (r_type != ResponseType::SUCCESS) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to fetch info for leaderboard ID {}.", leaderboard_id); - rc_api_destroy_fetch_leaderboard_info_response(&board_info); - return r_type; - } - lboard.name = board_info.title; - lboard.description = board_info.description; - lboard.entries.clear(); - for (u32 i = 0; i < board_info.num_entries; ++i) - { - const auto& org_entry = board_info.entries[i]; - auto dest_entry = LeaderboardEntry{ - .username = org_entry.username, - .rank = org_entry.rank, - }; - if (rc_runtime_format_lboard_value(dest_entry.score.data(), FORMAT_SIZE, org_entry.score, - board_info.format) == 0) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format leaderboard score {}.", org_entry.score); - strncpy(dest_entry.score.data(), fmt::format("{}", org_entry.score).c_str(), FORMAT_SIZE); - } - lboard.entries.insert_or_assign(org_entry.index, std::move(dest_entry)); - } - rc_api_destroy_fetch_leaderboard_info_response(&board_info); - } - - { - // Retrieve, if exists, the player's entry, the two entries above the player, and the two - // entries below the player, for a total of five entries. Technically I only need one entry - // below, but the API is ambiguous what happens if an even number and a username are provided. - rc_api_fetch_leaderboard_info_response_t board_info{}; - const rc_api_fetch_leaderboard_info_request_t fetch_board_request = { - .leaderboard_id = leaderboard_id, - .count = 5, - .first_entry = 0, - .username = username.c_str()}; - const ResponseType r_type = - Request( - fetch_board_request, &board_info, rc_api_init_fetch_leaderboard_info_request, - rc_api_process_fetch_leaderboard_info_response); - if (r_type != ResponseType::SUCCESS) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to fetch info for leaderboard ID {}.", leaderboard_id); - rc_api_destroy_fetch_leaderboard_info_response(&board_info); - return r_type; - } - for (u32 i = 0; i < board_info.num_entries; ++i) - { - const auto& org_entry = board_info.entries[i]; - auto dest_entry = LeaderboardEntry{ - .username = org_entry.username, - .rank = org_entry.rank, - }; - if (rc_runtime_format_lboard_value(dest_entry.score.data(), FORMAT_SIZE, org_entry.score, - board_info.format) == 0) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format leaderboard score {}.", org_entry.score); - strncpy(dest_entry.score.data(), fmt::format("{}", org_entry.score).c_str(), FORMAT_SIZE); - } - lboard.entries.insert_or_assign(org_entry.index, std::move(dest_entry)); - if (org_entry.username == username) - lboard.player_index = org_entry.index; - } - rc_api_destroy_fetch_leaderboard_info_response(&board_info); - } - - { - std::lock_guard lg{m_lock}; - m_leaderboard_map.insert_or_assign(leaderboard_id, std::move(lboard)); - } - - return ResponseType::SUCCESS; -} - -void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool enabled, - bool unofficial, bool encore) -{ - auto it = m_unlock_map.find(id); - if (it == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted to unlock unknown achievement id {}.", id); + WARN_LOG_FMT(ACHIEVEMENTS, "Failed to login {} to RetroAchievements server.", + Config::Get(Config::RA_USERNAME)); return; } - const UnlockStatus& status = it->second; - u32 index = status.game_data_index; - bool active = (rc_runtime_get_achievement(&m_runtime, id) != nullptr); - bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED); - // Deactivate achievements if game is not loaded - bool activate = m_is_game_loaded; - // Activate achievements only if achievements are enabled - if (activate && !enabled) - activate = false; - // Deactivate if achievement is unofficial, unless unofficial achievements are enabled - if (activate && !unofficial && - m_game_data.achievements[index].category == RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL) + const rc_client_user_t* user; { - activate = false; + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; + user = rc_client_get_user_info(client); } - // If encore mode is on, activate/deactivate regardless of current unlock status - if (activate && !encore) + if (!user) { - // Encore is off, achievement has been unlocked in this session, deactivate - activate = (status.session_unlock_count == 0); - // Encore is off, achievement has been hardcore unlocked on site, deactivate - if (activate && status.remote_unlock_status == UnlockStatus::UnlockType::HARDCORE) - activate = false; - // Encore is off, hardcore is off, achievement has been softcore unlocked on site, deactivate - if (activate && !hardcore_mode_enabled && - status.remote_unlock_status == UnlockStatus::UnlockType::SOFTCORE) + WARN_LOG_FMT(ACHIEVEMENTS, "Failed to retrieve user information from client."); + return; + } + + std::string config_username = Config::Get(Config::RA_USERNAME); + if (config_username != user->username) + { + if (Common::CaseInsensitiveEquals(config_username, user->username)) { - activate = false; + INFO_LOG_FMT(ACHIEVEMENTS, + "Case mismatch between site {} and local {}; updating local config.", + user->username, Config::Get(Config::RA_USERNAME)); + Config::SetBaseOrCurrent(Config::RA_USERNAME, user->username); + } + else + { + INFO_LOG_FMT(ACHIEVEMENTS, "Attempted to login prior user {}; current user is {}.", + user->username, Config::Get(Config::RA_USERNAME)); + rc_client_logout(client); + return; } } + INFO_LOG_FMT(ACHIEVEMENTS, "Successfully logged in {} to RetroAchievements server.", + user->username); + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; + Config::SetBaseOrCurrent(Config::RA_API_TOKEN, user->token); + AchievementManager::GetInstance().FetchPlayerBadge(); +} - if (!active && activate) +void AchievementManager::FetchBoardInfo(AchievementId leaderboard_id) +{ + u32* callback_data_1 = new u32(leaderboard_id); + u32* callback_data_2 = new u32(leaderboard_id); + rc_client_begin_fetch_leaderboard_entries(m_client, leaderboard_id, 1, 4, + LeaderboardEntriesCallback, callback_data_1); + rc_client_begin_fetch_leaderboard_entries_around_user( + m_client, leaderboard_id, 4, LeaderboardEntriesCallback, callback_data_2); +} + +void AchievementManager::LeaderboardEntriesCallback(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, + rc_client_t* client, void* userdata) +{ + if (result != RC_OK) { - rc_runtime_activate_achievement(&m_runtime, id, m_game_data.achievements[index].definition, - nullptr, 0); + WARN_LOG_FMT(ACHIEVEMENTS, "Failed to fetch leaderboard entries."); + return; } - if (active && !activate) - rc_runtime_deactivate_achievement(&m_runtime, id); -} -void AchievementManager::GenerateRichPresence(const Core::CPUThreadGuard& guard) -{ - std::lock_guard lg{m_lock}; - rc_runtime_get_richpresence( - &m_runtime, m_rich_presence.data(), RP_SIZE, - [](unsigned address, unsigned num_bytes, void* ud) { - return static_cast(ud)->MemoryPeeker(address, num_bytes, ud); - }, - this, nullptr); -} - -AchievementManager::ResponseType AchievementManager::AwardAchievement(AchievementId achievement_id) -{ - std::string username = Config::Get(Config::RA_USERNAME); - std::string api_token = Config::Get(Config::RA_API_TOKEN); - bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED); - rc_api_award_achievement_request_t award_request = {.username = username.c_str(), - .api_token = api_token.c_str(), - .achievement_id = achievement_id, - .hardcore = hardcore_mode_enabled, - .game_hash = m_game_hash.data()}; - rc_api_award_achievement_response_t award_response = {}; - ResponseType r_type = - Request( - award_request, &award_response, rc_api_init_award_achievement_request, - rc_api_process_award_achievement_response); - rc_api_destroy_award_achievement_response(&award_response); - if (r_type == ResponseType::SUCCESS) + u32 leaderboard_id = *reinterpret_cast(userdata); + delete userdata; + auto& leaderboard = AchievementManager::GetInstance().m_leaderboard_map[leaderboard_id]; + for (size_t ix = 0; ix < list->num_entries; ix++) { - INFO_LOG_FMT(ACHIEVEMENTS, "Awarded achievement ID {}.", achievement_id); + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; + const auto& response_entry = list->entries[ix]; + auto& map_entry = leaderboard.entries[response_entry.index]; + map_entry.username.assign(response_entry.user); + memcpy(map_entry.score.data(), response_entry.display, FORMAT_SIZE); + map_entry.rank = response_entry.rank; + } + AchievementManager::GetInstance().m_update_callback({.leaderboards = {leaderboard_id}}); +} + +void AchievementManager::LoadGameCallback(int result, const char* error_message, + rc_client_t* client, void* userdata) +{ + if (result != RC_OK) + { + WARN_LOG_FMT(ACHIEVEMENTS, "Failed to load data for current game."); + return; + } + + auto* game = rc_client_get_game_info(client); + if (!game) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to retrieve game information from client."); + return; + } + INFO_LOG_FMT(ACHIEVEMENTS, "Loaded data for game ID {}.", game->id); + + AchievementManager::GetInstance().FetchGameBadges(); + AchievementManager::GetInstance().m_system = &Core::System::GetInstance(); + AchievementManager::GetInstance().m_update_callback({.all = true}); + // Set this to a value that will immediately trigger RP + AchievementManager::GetInstance().m_last_rp_time = + std::chrono::steady_clock::now() - std::chrono::minutes{2}; +} + +void AchievementManager::ChangeMediaCallback(int result, const char* error_message, + rc_client_t* client, void* userdata) +{ + if (result == RC_OK) + return; + + if (result == RC_HARDCORE_DISABLED) + { + WARN_LOG_FMT(ACHIEVEMENTS, "Hardcore disabled. Unrecognized media inserted."); } else { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to award achievement ID {}.", achievement_id); - } - return r_type; -} + if (!error_message) + error_message = rc_error_str(result); -AchievementManager::ResponseType AchievementManager::SubmitLeaderboard(AchievementId leaderboard_id, - int value) -{ - std::string username = Config::Get(Config::RA_USERNAME); - std::string api_token = Config::Get(Config::RA_API_TOKEN); - rc_api_submit_lboard_entry_request_t submit_request = {.username = username.c_str(), - .api_token = api_token.c_str(), - .leaderboard_id = leaderboard_id, - .score = value, - .game_hash = m_game_hash.data()}; - rc_api_submit_lboard_entry_response_t submit_response = {}; - ResponseType r_type = - Request( - submit_request, &submit_response, rc_api_init_submit_lboard_entry_request, - rc_api_process_submit_lboard_entry_response); - rc_api_destroy_submit_lboard_entry_response(&submit_response); - if (r_type == ResponseType::SUCCESS) - { - INFO_LOG_FMT(ACHIEVEMENTS, "Submitted leaderboard ID {}.", leaderboard_id); + ERROR_LOG_FMT(ACHIEVEMENTS, "RetroAchievements media change failed: {}", error_message); } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "Failed to submit leaderboard ID {}.", leaderboard_id); - } - return r_type; -} - -AchievementManager::ResponseType -AchievementManager::PingRichPresence(const RichPresence& rich_presence) -{ - std::string username = Config::Get(Config::RA_USERNAME); - std::string api_token = Config::Get(Config::RA_API_TOKEN); - rc_api_ping_request_t ping_request = {.username = username.c_str(), - .api_token = api_token.c_str(), - .game_id = m_game_id, - .rich_presence = rich_presence.data()}; - rc_api_ping_response_t ping_response = {}; - ResponseType r_type = Request( - ping_request, &ping_response, rc_api_init_ping_request, rc_api_process_ping_response); - rc_api_destroy_ping_response(&ping_response); - return r_type; } void AchievementManager::DisplayWelcomeMessage() { std::lock_guard lg{m_lock}; - PointSpread spread = TallyScore(); - if (Config::Get(Config::RA_HARDCORE_ENABLED)) + const u32 color = + rc_client_get_hardcore_enabled(m_client) ? OSD::Color::YELLOW : OSD::Color::CYAN; + if (Config::Get(Config::RA_BADGES_ENABLED)) + { + OSD::AddMessage("", OSD::Duration::VERY_LONG, OSD::Color::GREEN, + DecodeBadgeToOSDIcon(m_game_badge.badge)); + } + auto info = rc_client_get_game_info(m_client); + if (!info) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Attempting to welcome player to game not running."); + return; + } + OSD::AddMessage(info->title, OSD::Duration::VERY_LONG, OSD::Color::GREEN); + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(m_client, &summary); + OSD::AddMessage(fmt::format("You have {}/{} achievements worth {}/{} points", + summary.num_unlocked_achievements, summary.num_core_achievements, + summary.points_unlocked, summary.points_core), + OSD::Duration::VERY_LONG, color); + if (summary.num_unsupported_achievements > 0) { OSD::AddMessage( - fmt::format("You have {}/{} achievements worth {}/{} points", spread.hard_unlocks, - spread.total_count, spread.hard_points, spread.total_points), - OSD::Duration::VERY_LONG, OSD::Color::YELLOW, - (Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) : - nullptr); - OSD::AddMessage("Hardcore mode is ON", OSD::Duration::VERY_LONG, OSD::Color::YELLOW); - } - else - { - OSD::AddMessage(fmt::format("You have {}/{} achievements worth {}/{} points", - spread.hard_unlocks + spread.soft_unlocks, spread.total_count, - spread.hard_points + spread.soft_points, spread.total_points), - OSD::Duration::VERY_LONG, OSD::Color::CYAN, - (Config::Get(Config::RA_BADGES_ENABLED)) ? - DecodeBadgeToOSDIcon(m_game_badge.badge) : - nullptr); - OSD::AddMessage("Hardcore mode is OFF", OSD::Duration::VERY_LONG, OSD::Color::CYAN); - } -} - -void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event) -{ - bool hardcore_mode_enabled = Config::Get(Config::RA_HARDCORE_ENABLED); - const auto event_id = runtime_event->id; - auto it = m_unlock_map.find(event_id); - if (it == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement triggered event with id {}.", event_id); - return; - } - it->second.session_unlock_count++; - AchievementId game_data_index = it->second.game_data_index; - OSD::AddMessage(fmt::format("Unlocked: {} ({})", m_game_data.achievements[game_data_index].title, - m_game_data.achievements[game_data_index].points), - OSD::Duration::VERY_LONG, - (hardcore_mode_enabled) ? OSD::Color::YELLOW : OSD::Color::CYAN, - (Config::Get(Config::RA_BADGES_ENABLED)) ? - DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) : - nullptr); - if (m_game_data.achievements[game_data_index].category == RC_ACHIEVEMENT_CATEGORY_CORE) - { - m_queue.EmplaceItem([this, event_id] { AwardAchievement(event_id); }); - PointSpread spread = TallyScore(); - if (spread.hard_points == spread.total_points && - it->second.remote_unlock_status != UnlockStatus::UnlockType::HARDCORE) - { - OSD::AddMessage( - fmt::format("Congratulations! {} has mastered {}", m_display_name, m_game_data.title), - OSD::Duration::VERY_LONG, OSD::Color::YELLOW, - (Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) : - nullptr); - } - else if (spread.hard_points + spread.soft_points == spread.total_points && - it->second.remote_unlock_status == UnlockStatus::UnlockType::LOCKED) - { - OSD::AddMessage( - fmt::format("Congratulations! {} has completed {}", m_display_name, m_game_data.title), - OSD::Duration::VERY_LONG, OSD::Color::CYAN, - (Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(m_game_badge.badge) : - nullptr); - } - } - ActivateDeactivateAchievement(event_id, Config::Get(Config::RA_ACHIEVEMENTS_ENABLED), - Config::Get(Config::RA_UNOFFICIAL_ENABLED), - Config::Get(Config::RA_ENCORE_ENABLED)); -} - -void AchievementManager::HandleAchievementProgressUpdatedEvent( - const rc_runtime_event_t* runtime_event) -{ - if (!Config::Get(Config::RA_PROGRESS_ENABLED)) - return; - auto it = m_unlock_map.find(runtime_event->id); - if (it == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement progress updated event with id {}.", - runtime_event->id); - return; - } - AchievementId game_data_index = it->second.game_data_index; - FormattedValue value{}; - if (rc_runtime_format_achievement_measured(&m_runtime, runtime_event->id, value.data(), - FORMAT_SIZE) == 0) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to format measured data {}.", value.data()); - return; + fmt::format("{} achievements unsupported", summary.num_unsupported_achievements), + OSD::Duration::VERY_LONG, OSD::Color::RED); } OSD::AddMessage( - fmt::format("{} {}", m_game_data.achievements[game_data_index].title, value.data()), - OSD::Duration::SHORT, OSD::Color::GREEN, - (Config::Get(Config::RA_BADGES_ENABLED)) ? - DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) : - nullptr); + fmt::format("Hardcore mode is {}", rc_client_get_hardcore_enabled(m_client) ? "ON" : "OFF"), + OSD::Duration::VERY_LONG, color); + OSD::AddMessage(fmt::format("Leaderboard submissions are {}", + rc_client_get_hardcore_enabled(m_client) ? "ON" : "OFF"), + OSD::Duration::VERY_LONG, color); } -void AchievementManager::HandleAchievementPrimedEvent(const rc_runtime_event_t* runtime_event) +void AchievementManager::HandleAchievementTriggeredEvent(const rc_client_event_t* client_event) { - if (!Config::Get(Config::RA_BADGES_ENABLED)) - return; - auto it = m_unlock_map.find(runtime_event->id); - if (it == m_unlock_map.end()) + OSD::AddMessage(fmt::format("Unlocked: {} ({})", client_event->achievement->title, + client_event->achievement->points), + OSD::Duration::VERY_LONG, + (rc_client_get_hardcore_enabled(AchievementManager::GetInstance().m_client)) ? + OSD::Color::YELLOW : + OSD::Color::CYAN, + (Config::Get(Config::RA_BADGES_ENABLED)) ? + DecodeBadgeToOSDIcon(AchievementManager::GetInstance() + .m_unlocked_badges[client_event->achievement->id] + .badge) : + nullptr); +} + +void AchievementManager::HandleLeaderboardStartedEvent(const rc_client_event_t* client_event) +{ + OSD::AddMessage(fmt::format("Attempting leaderboard: {} - {}", client_event->leaderboard->title, + client_event->leaderboard->description), + OSD::Duration::VERY_LONG, OSD::Color::GREEN); + AchievementManager::GetInstance().FetchBoardInfo(client_event->leaderboard->id); +} + +void AchievementManager::HandleLeaderboardFailedEvent(const rc_client_event_t* client_event) +{ + OSD::AddMessage(fmt::format("Failed leaderboard: {}", client_event->leaderboard->title), + OSD::Duration::VERY_LONG, OSD::Color::RED); + AchievementManager::GetInstance().FetchBoardInfo(client_event->leaderboard->id); +} + +void AchievementManager::HandleLeaderboardSubmittedEvent(const rc_client_event_t* client_event) +{ + OSD::AddMessage(fmt::format("Scored {} on leaderboard: {}", + client_event->leaderboard->tracker_value, + client_event->leaderboard->title), + OSD::Duration::VERY_LONG, OSD::Color::YELLOW); + AchievementManager::GetInstance().FetchBoardInfo(client_event->leaderboard->id); +} + +void AchievementManager::HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* client_event) +{ + auto& active_leaderboards = AchievementManager::GetInstance().m_active_leaderboards; + for (auto& leaderboard : active_leaderboards) { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement primed event with id {}.", runtime_event->id); + if (leaderboard.id == client_event->leaderboard_tracker->id) + { + strncpy(leaderboard.display, client_event->leaderboard_tracker->display, + RC_CLIENT_LEADERBOARD_DISPLAY_SIZE); + } + } +} + +void AchievementManager::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* client_event) +{ + AchievementManager::GetInstance().m_active_leaderboards.push_back( + *client_event->leaderboard_tracker); +} + +void AchievementManager::HandleLeaderboardTrackerHideEvent(const rc_client_event_t* client_event) +{ + auto& active_leaderboards = AchievementManager::GetInstance().m_active_leaderboards; + std::erase_if(active_leaderboards, [client_event](const auto& leaderboard) { + return leaderboard.id == client_event->leaderboard_tracker->id; + }); +} + +void AchievementManager::HandleAchievementChallengeIndicatorShowEvent( + const rc_client_event_t* client_event) +{ + if (Config::Get(Config::RA_BADGES_ENABLED)) + { + auto& unlocked_badges = AchievementManager::GetInstance().m_unlocked_badges; + if (const auto unlocked_iter = unlocked_badges.find(client_event->achievement->id); + unlocked_iter != unlocked_badges.end()) + { + AchievementManager::GetInstance().m_active_challenges[client_event->achievement->badge_name] = + DecodeBadgeToOSDIcon(unlocked_iter->second.badge); + } + } +} + +void AchievementManager::HandleAchievementChallengeIndicatorHideEvent( + const rc_client_event_t* client_event) +{ + AchievementManager::GetInstance().m_active_challenges.erase( + client_event->achievement->badge_name); +} + +void AchievementManager::HandleAchievementProgressIndicatorShowEvent( + const rc_client_event_t* client_event) +{ + OSD::AddMessage(fmt::format("{} {}", client_event->achievement->title, + client_event->achievement->measured_progress), + OSD::Duration::SHORT, OSD::Color::GREEN, + (Config::Get(Config::RA_BADGES_ENABLED)) ? + DecodeBadgeToOSDIcon(AchievementManager::GetInstance() + .m_unlocked_badges[client_event->achievement->id] + .badge) : + nullptr); +} + +void AchievementManager::HandleGameCompletedEvent(const rc_client_event_t* client_event, + rc_client_t* client) +{ + auto* user_info = rc_client_get_user_info(client); + auto* game_info = rc_client_get_game_info(client); + if (!user_info || !game_info) + { + WARN_LOG_FMT(ACHIEVEMENTS, "Received Game Completed event when game not running."); return; } - m_active_challenges[it->second.unlocked_badge.name] = - DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge); + bool hardcore = rc_client_get_hardcore_enabled(client); + OSD::AddMessage(fmt::format("Congratulations! {} has {} {}", user_info->display_name, + hardcore ? "mastered" : "completed", game_info->title), + OSD::Duration::VERY_LONG, hardcore ? OSD::Color::YELLOW : OSD::Color::CYAN, + (Config::Get(Config::RA_BADGES_ENABLED)) ? + DecodeBadgeToOSDIcon(AchievementManager::GetInstance().m_game_badge.badge) : + nullptr); } -void AchievementManager::HandleAchievementUnprimedEvent(const rc_runtime_event_t* runtime_event) +void AchievementManager::HandleResetEvent(const rc_client_event_t* client_event) { - if (!Config::Get(Config::RA_BADGES_ENABLED)) - return; - auto it = m_unlock_map.find(runtime_event->id); - if (it == m_unlock_map.end()) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid achievement unprimed event with id {}.", - runtime_event->id); - return; - } - m_active_challenges.erase(it->second.unlocked_badge.name); + INFO_LOG_FMT(ACHIEVEMENTS, "Reset requested by Achievement Mananger"); + Core::Stop(Core::System::GetInstance()); } -void AchievementManager::HandleLeaderboardStartedEvent(const rc_runtime_event_t* runtime_event) +void AchievementManager::HandleServerErrorEvent(const rc_client_event_t* client_event) { - for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++) - { - if (m_game_data.leaderboards[ix].id == runtime_event->id) - { - OSD::AddMessage(fmt::format("Attempting leaderboard: {}", m_game_data.leaderboards[ix].title), - OSD::Duration::VERY_LONG, OSD::Color::GREEN); - return; - } - } - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard started event with id {}.", runtime_event->id); -} - -void AchievementManager::HandleLeaderboardCanceledEvent(const rc_runtime_event_t* runtime_event) -{ - for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++) - { - if (m_game_data.leaderboards[ix].id == runtime_event->id) - { - OSD::AddMessage(fmt::format("Failed leaderboard: {}", m_game_data.leaderboards[ix].title), - OSD::Duration::VERY_LONG, OSD::Color::RED); - return; - } - } - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard canceled event with id {}.", runtime_event->id); -} - -void AchievementManager::HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event) -{ - const auto event_id = runtime_event->id; - const auto event_value = runtime_event->value; - m_queue.EmplaceItem([this, event_id, event_value] { SubmitLeaderboard(event_id, event_value); }); - for (u32 ix = 0; ix < m_game_data.num_leaderboards; ix++) - { - if (m_game_data.leaderboards[ix].id == event_id) - { - FormattedValue value{}; - rc_runtime_format_lboard_value(value.data(), static_cast(value.size()), event_value, - m_game_data.leaderboards[ix].format); - if (std::find(value.begin(), value.end(), '\0') == value.end()) - { - OSD::AddMessage(fmt::format("Scored {} on leaderboard: {}", - std::string_view{value.data(), value.size()}, - m_game_data.leaderboards[ix].title), - OSD::Duration::VERY_LONG, OSD::Color::YELLOW); - } - else - { - OSD::AddMessage(fmt::format("Scored {} on leaderboard: {}", value.data(), - m_game_data.leaderboards[ix].title), - OSD::Duration::VERY_LONG, OSD::Color::YELLOW); - } - m_queue.EmplaceItem([this, event_id] { - FetchBoardInfo(event_id); - m_update_callback(); - }); - break; - } - } - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid leaderboard triggered event with id {}.", event_id); -} - -// Every RetroAchievements API call, with only a partial exception for fetch_image, follows -// the same design pattern (here, X is the name of the call): -// Create a specific rc_api_X_request_t struct and populate with the necessary values -// Call rc_api_init_X_request to convert this into a generic rc_api_request_t struct -// Perform the HTTP request using the url and post_data in the rc_api_request_t struct -// Call rc_api_process_X_response to convert the raw string HTTP response into a -// rc_api_X_response_t struct -// Use the data in the rc_api_X_response_t struct as needed -// Call rc_api_destroy_X_response when finished with the response struct to free memory -template -AchievementManager::ResponseType AchievementManager::Request( - RcRequest rc_request, RcResponse* rc_response, - const std::function& init_request, - const std::function& process_response) -{ - rc_api_request_t api_request; - Common::HttpRequest http_request; - if (init_request(&api_request, &rc_request) != RC_OK || !api_request.post_data) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid API request."); - return ResponseType::INVALID_REQUEST; - } - auto http_response = http_request.Post(api_request.url, api_request.post_data); - rc_api_destroy_request(&api_request); - if (http_response.has_value() && http_response->size() > 0) - { - const std::string response_str(http_response->begin(), http_response->end()); - if (process_response(rc_response, response_str.c_str()) != RC_OK) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to process HTTP response. \nURL: {} \nresponse: {}", - api_request.url, response_str); - return ResponseType::MALFORMED_OBJECT; - } - if (rc_response->response.succeeded) - { - return ResponseType::SUCCESS; - } - else - { - Logout(); - WARN_LOG_FMT(ACHIEVEMENTS, "Invalid RetroAchievements credentials; failed login."); - return ResponseType::INVALID_CREDENTIALS; - } - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "RetroAchievements connection failed. \nURL: {}", api_request.url); - return ResponseType::CONNECTION_FAILED; - } -} - -AchievementManager::ResponseType -AchievementManager::RequestImage(rc_api_fetch_image_request_t rc_request, Badge* rc_response) -{ - rc_api_request_t api_request; - Common::HttpRequest http_request; - if (rc_api_init_fetch_image_request(&api_request, &rc_request) != RC_OK) - { - ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid request for image."); - return ResponseType::INVALID_REQUEST; - } - auto http_response = http_request.Get(api_request.url); - if (http_response.has_value() && http_response->size() > 0) - { - rc_api_destroy_request(&api_request); - *rc_response = std::move(*http_response); - return ResponseType::SUCCESS; - } - else - { - WARN_LOG_FMT(ACHIEVEMENTS, "RetroAchievements connection failed on image request.\n URL: {}", - api_request.url); - rc_api_destroy_request(&api_request); - return ResponseType::CONNECTION_FAILED; - } + ERROR_LOG_FMT(ACHIEVEMENTS, "RetroAchievements server error: {} {}", + client_event->server_error->api, client_event->server_error->error_message); } static std::unique_ptr DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge) @@ -1706,4 +812,175 @@ static std::unique_ptr DecodeBadgeToOSDIcon(const AchievementManager: return icon; } +void AchievementManager::Request(const rc_api_request_t* request, + rc_client_server_callback_t callback, void* callback_data, + rc_client_t* client) +{ + std::string url = request->url; + std::string post_data = request->post_data; + AchievementManager::GetInstance().m_queue.EmplaceItem([url = std::move(url), + post_data = std::move(post_data), + callback = std::move(callback), + callback_data = std::move(callback_data)] { + const Common::HttpRequest::Headers USER_AGENT_HEADER = {{"User-Agent", "Dolphin/Placeholder"}}; + + Common::HttpRequest http_request; + Common::HttpRequest::Response http_response; + if (!post_data.empty()) + { + http_response = http_request.Post(url, post_data, USER_AGENT_HEADER, + Common::HttpRequest::AllowedReturnCodes::All); + } + else + { + http_response = + http_request.Get(url, USER_AGENT_HEADER, Common::HttpRequest::AllowedReturnCodes::All); + } + + rc_api_server_response_t server_response; + if (http_response.has_value() && http_response->size() > 0) + { + server_response.body = reinterpret_cast(http_response->data()); + server_response.body_length = http_response->size(); + server_response.http_status_code = http_request.GetLastResponseCode(); + } + else + { + constexpr char error_message[] = "Failed HTTP request."; + server_response.body = error_message; + server_response.body_length = sizeof(error_message); + server_response.http_status_code = RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR; + } + + callback(&server_response, callback_data); + }); +} + +u32 AchievementManager::MemoryPeeker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client) +{ + if (buffer == nullptr) + return 0u; + auto& system = Core::System::GetInstance(); + Core::CPUThreadGuard threadguard(system); + for (u32 num_read = 0; num_read < num_bytes; num_read++) + { + auto value = system.GetMMU().HostTryReadU8(threadguard, address + num_read, + PowerPC::RequestedAddressSpace::Physical); + if (!value.has_value()) + return num_read; + buffer[num_read] = value.value().value; + } + return num_bytes; +} + +void AchievementManager::FetchBadge(AchievementManager::BadgeStatus* badge, u32 badge_type, + const AchievementManager::BadgeNameFunction function, + const UpdatedItems callback_data) +{ + if (!m_client || !HasAPIToken() || !Config::Get(Config::RA_BADGES_ENABLED)) + { + m_update_callback(callback_data); + return; + } + + m_image_queue.EmplaceItem([this, badge, badge_type, function = std::move(function), + callback_data = std::move(callback_data)] { + std::string name_to_fetch; + { + std::lock_guard lg{m_lock}; + name_to_fetch = function(*this); + if (name_to_fetch.empty()) + return; + } + rc_api_fetch_image_request_t icon_request = {.image_name = name_to_fetch.c_str(), + .image_type = badge_type}; + Badge fetched_badge; + rc_api_request_t api_request; + Common::HttpRequest http_request; + if (rc_api_init_fetch_image_request(&api_request, &icon_request) != RC_OK) + { + ERROR_LOG_FMT(ACHIEVEMENTS, "Invalid request for image {}.", name_to_fetch); + return; + } + auto http_response = http_request.Get(api_request.url); + if (http_response.has_value() && http_response->size() <= 0) + { + WARN_LOG_FMT(ACHIEVEMENTS, "RetroAchievements connection failed on image request.\n URL: {}", + api_request.url); + rc_api_destroy_request(&api_request); + m_update_callback(callback_data); + return; + } + + rc_api_destroy_request(&api_request); + fetched_badge = std::move(*http_response); + + INFO_LOG_FMT(ACHIEVEMENTS, "Successfully downloaded badge id {}.", name_to_fetch); + std::lock_guard lg{m_lock}; + if (function(*this).empty() || name_to_fetch != function(*this)) + { + INFO_LOG_FMT(ACHIEVEMENTS, "Requested outdated badge id {}.", name_to_fetch); + return; + } + badge->badge = std::move(fetched_badge); + badge->name = std::move(name_to_fetch); + + m_update_callback(callback_data); + }); +} + +void AchievementManager::EventHandler(const rc_client_event_t* event, rc_client_t* client) +{ + switch (event->type) + { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + HandleAchievementTriggeredEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + HandleLeaderboardStartedEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + HandleLeaderboardFailedEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + HandleLeaderboardSubmittedEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + HandleLeaderboardTrackerUpdateEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + HandleLeaderboardTrackerShowEvent(event); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + HandleLeaderboardTrackerHideEvent(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + HandleAchievementChallengeIndicatorShowEvent(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + HandleAchievementChallengeIndicatorHideEvent(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + HandleAchievementProgressIndicatorShowEvent(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + // OnScreenDisplay messages disappear over time, so this is unnecessary + // unless the display algorithm changes in the future. + break; + case RC_CLIENT_EVENT_GAME_COMPLETED: + HandleGameCompletedEvent(event, client); + break; + case RC_CLIENT_EVENT_RESET: + HandleResetEvent(event); + break; + case RC_CLIENT_EVENT_SERVER_ERROR: + HandleServerErrorEvent(event); + break; + default: + INFO_LOG_FMT(ACHIEVEMENTS, "Event triggered of unhandled type {}", event->type); + break; + } +} + #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 770e64c18c..757daeb86d 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -15,9 +15,11 @@ #include #include +#include #include #include "Common/Event.h" +#include "Common/HttpRequest.h" #include "Common/WorkQueueThread.h" #include "DiscIO/Volume.h" @@ -35,30 +37,7 @@ struct Icon; class AchievementManager { public: - enum class ResponseType - { - SUCCESS, - NOT_ENABLED, - MANAGER_NOT_INITIALIZED, - INVALID_REQUEST, - INVALID_CREDENTIALS, - CONNECTION_FAILED, - MALFORMED_OBJECT, - EXPIRED_CONTEXT, - UNKNOWN_FAILURE - }; - using ResponseCallback = std::function; - using UpdateCallback = std::function; - - struct PointSpread - { - u32 total_count; - u32 total_points; - u32 hard_unlocks; - u32 hard_points; - u32 soft_unlocks; - u32 soft_points; - }; + using BadgeNameFunction = std::function; static constexpr size_t HASH_SIZE = 33; using Hash = std::array; @@ -70,6 +49,7 @@ public: using RichPresence = std::array; using Badge = std::vector; using NamedIconMap = std::map, std::less<>>; + static constexpr size_t MAX_DISPLAYED_LBOARDS = 4; struct BadgeStatus { @@ -77,22 +57,6 @@ public: Badge badge{}; }; - struct UnlockStatus - { - AchievementId game_data_index = 0; - enum class UnlockType - { - LOCKED, - SOFTCORE, - HARDCORE - } remote_unlock_status = UnlockType::LOCKED; - u32 session_unlock_count = 0; - u32 points = 0; - BadgeStatus locked_badge; - BadgeStatus unlocked_badge; - u32 category = RC_ACHIEVEMENT_CATEGORY_CORE; - }; - static constexpr std::string_view GRAY = "transparent"; static constexpr std::string_view GOLD = "#FFD700"; static constexpr std::string_view BLUE = "#0B71C1"; @@ -112,43 +76,50 @@ public: std::unordered_map entries; }; + struct UpdatedItems + { + bool all = false; + bool player_icon = false; + bool game_icon = false; + bool all_achievements = false; + std::set achievements{}; + bool all_leaderboards = false; + std::set leaderboards{}; + bool rich_presence = false; + }; + using UpdateCallback = std::function; + static AchievementManager& GetInstance(); void Init(); void SetUpdateCallback(UpdateCallback callback); - ResponseType Login(const std::string& password); - void LoginAsync(const std::string& password, const ResponseCallback& callback); - bool IsLoggedIn() const; - void HashGame(const std::string& file_path, const ResponseCallback& callback); - void HashGame(const DiscIO::Volume* volume, const ResponseCallback& callback); + void Login(const std::string& password); + bool HasAPIToken() const; + void LoadGame(const std::string& file_path, const DiscIO::Volume* volume); bool IsGameLoaded() const; - void LoadUnlockData(const ResponseCallback& callback); - void ActivateDeactivateAchievements(); - void ActivateDeactivateLeaderboards(); - void ActivateDeactivateRichPresence(); - void FetchBadges(); + void FetchPlayerBadge(); + void FetchGameBadges(); void DoFrame(); - u32 MemoryPeeker(u32 address, u32 num_bytes, void* ud); - void AchievementEventHandler(const rc_runtime_event_t* runtime_event); std::recursive_mutex& GetLock(); + void SetHardcoreMode(); bool IsHardcoreModeActive() const; - std::string GetPlayerDisplayName() const; + void SetSpectatorMode(); + std::string_view GetPlayerDisplayName() const; u32 GetPlayerScore() const; const BadgeStatus& GetPlayerBadge() const; - std::string GetGameDisplayName() const; - PointSpread TallyScore() const; + std::string_view GetGameDisplayName() const; + rc_client_t* GetClient(); rc_api_fetch_game_data_response_t* GetGameData(); const BadgeStatus& GetGameBadge() const; - const UnlockStatus& GetUnlockStatus(AchievementId achievement_id) const; - AchievementManager::ResponseType GetAchievementProgress(AchievementId achievement_id, u32* value, - u32* target); - const std::unordered_map& GetLeaderboardsInfo() const; + const BadgeStatus& GetAchievementBadge(AchievementId id, bool locked) const; + const LeaderboardStatus* GetLeaderboardInfo(AchievementId leaderboard_id); RichPresence GetRichPresence() const; - bool IsDisabled() const { return m_disabled; }; - void SetDisabled(bool disabled); const NamedIconMap& GetChallengeIcons() const; + std::vector GetActiveLeaderboards() const; + + void DoState(PointerWrap& p); void CloseGame(); void Logout(); @@ -163,6 +134,8 @@ private: std::unique_ptr volume; }; + const BadgeStatus m_default_badge; + static void* FilereaderOpenByFilepath(const char* path_utf8); static void* FilereaderOpenByVolume(const char* path_utf8); static void FilereaderSeek(void* file_handle, int64_t offset, int origin); @@ -170,47 +143,50 @@ private: static size_t FilereaderRead(void* file_handle, void* buffer, size_t requested_bytes); static void FilereaderClose(void* file_handle); - ResponseType VerifyCredentials(const std::string& password); - ResponseType ResolveHash(const Hash& game_hash, u32* game_id); - void LoadGameSync(const ResponseCallback& callback); - ResponseType StartRASession(); - ResponseType FetchGameData(); - ResponseType FetchUnlockData(bool hardcore); - ResponseType FetchBoardInfo(AchievementId leaderboard_id); + static void LoginCallback(int result, const char* error_message, rc_client_t* client, + void* userdata); + + void FetchBoardInfo(AchievementId leaderboard_id); std::unique_ptr& GetLoadingVolume() { return m_loading_volume; }; - void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); - void GenerateRichPresence(const Core::CPUThreadGuard& guard); - - ResponseType AwardAchievement(AchievementId achievement_id); - ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value); - ResponseType PingRichPresence(const RichPresence& rich_presence); - + static void LoadGameCallback(int result, const char* error_message, rc_client_t* client, + void* userdata); + static void ChangeMediaCallback(int result, const char* error_message, rc_client_t* client, + void* userdata); void DisplayWelcomeMessage(); - void HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event); - void HandleAchievementProgressUpdatedEvent(const rc_runtime_event_t* runtime_event); - void HandleAchievementPrimedEvent(const rc_runtime_event_t* runtime_event); - void HandleAchievementUnprimedEvent(const rc_runtime_event_t* runtime_event); - void HandleLeaderboardStartedEvent(const rc_runtime_event_t* runtime_event); - void HandleLeaderboardCanceledEvent(const rc_runtime_event_t* runtime_event); - void HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event); + static void LeaderboardEntriesCallback(int result, const char* error_message, + rc_client_leaderboard_entry_list_t* list, + rc_client_t* client, void* userdata); - template - ResponseType Request(RcRequest rc_request, RcResponse* rc_response, - const std::function& init_request, - const std::function& process_response); - ResponseType RequestImage(rc_api_fetch_image_request_t rc_request, Badge* rc_response); + static void HandleAchievementTriggeredEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardStartedEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardFailedEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardSubmittedEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardTrackerShowEvent(const rc_client_event_t* client_event); + static void HandleLeaderboardTrackerHideEvent(const rc_client_event_t* client_event); + static void HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* client_event); + static void HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* client_event); + static void HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* client_event); + static void HandleGameCompletedEvent(const rc_client_event_t* client_event, rc_client_t* client); + static void HandleResetEvent(const rc_client_event_t* client_event); + static void HandleServerErrorEvent(const rc_client_event_t* client_event); + + static void Request(const rc_api_request_t* request, rc_client_server_callback_t callback, + void* callback_data, rc_client_t* client); + static u32 MemoryPeeker(u32 address, u8* buffer, u32 num_bytes, rc_client_t* client); + void FetchBadge(BadgeStatus* badge, u32 badge_type, const BadgeNameFunction function, + const UpdatedItems callback_data); + static void EventHandler(const rc_client_event_t* event, rc_client_t* client); rc_runtime_t m_runtime{}; + rc_client_t* m_client{}; Core::System* m_system{}; bool m_is_runtime_initialized = false; - UpdateCallback m_update_callback = [] {}; + UpdateCallback m_update_callback = [](const UpdatedItems&) {}; std::unique_ptr m_loading_volume; - bool m_disabled = false; - std::string m_display_name; - u32 m_player_score = 0; BadgeStatus m_player_badge; Hash m_game_hash{}; u32 m_game_id = 0; @@ -218,12 +194,14 @@ private: bool m_is_game_loaded = false; u32 m_framecount = 0; BadgeStatus m_game_badge; + std::unordered_map m_unlocked_badges; + std::unordered_map m_locked_badges; RichPresence m_rich_presence; - time_t m_last_ping_time = 0; + std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now(); - std::unordered_map m_unlock_map; std::unordered_map m_leaderboard_map; NamedIconMap m_active_challenges; + std::vector m_active_leaderboards; Common::WorkQueueThread> m_queue; Common::WorkQueueThread> m_image_queue; diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 538308ef4f..d57f4e74bb 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -576,8 +576,7 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard, } #ifdef USE_RETRO_ACHIEVEMENTS - AchievementManager::GetInstance().HashGame(executable.path, - [](AchievementManager::ResponseType r_type) {}); + AchievementManager::GetInstance().LoadGame(executable.path, nullptr); #endif // USE_RETRO_ACHIEVEMENTS if (!executable.reader->LoadIntoMemory(system)) diff --git a/Source/Core/Core/BootManager.cpp b/Source/Core/Core/BootManager.cpp index 9e9fb3b58c..6f7681fd30 100644 --- a/Source/Core/Core/BootManager.cpp +++ b/Source/Core/Core/BootManager.cpp @@ -167,7 +167,7 @@ bool BootCore(Core::System& system, std::unique_ptr boot, } #ifdef USE_RETRO_ACHIEVEMENTS - AchievementManager::GetInstance().SetDisabled(false); + AchievementManager::GetInstance().CloseGame(); #endif // USE_RETRO_ACHIEVEMENTS const bool load_ipl = !system.IsWii() && !Config::Get(Config::MAIN_SKIP_IPL) && diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp index 52502b5c20..38a5bb6166 100644 --- a/Source/Core/Core/Config/AchievementSettings.cpp +++ b/Source/Core/Core/Config/AchievementSettings.cpp @@ -15,20 +15,16 @@ const Info RA_ENABLED{{System::Achievements, "Achievements", "Enabled"}, f const Info RA_HOST_URL{{System::Achievements, "Achievements", "HostUrl"}, ""}; const Info RA_USERNAME{{System::Achievements, "Achievements", "Username"}, ""}; const Info RA_API_TOKEN{{System::Achievements, "Achievements", "ApiToken"}, ""}; -const Info RA_ACHIEVEMENTS_ENABLED{ - {System::Achievements, "Achievements", "AchievementsEnabled"}, false}; -const Info RA_LEADERBOARDS_ENABLED{ - {System::Achievements, "Achievements", "LeaderboardsEnabled"}, false}; -const Info RA_RICH_PRESENCE_ENABLED{ - {System::Achievements, "Achievements", "RichPresenceEnabled"}, false}; const Info RA_HARDCORE_ENABLED{{System::Achievements, "Achievements", "HardcoreEnabled"}, - false}; -const Info RA_PROGRESS_ENABLED{{System::Achievements, "Achievements", "ProgressEnabled"}, - false}; -const Info RA_BADGES_ENABLED{{System::Achievements, "Achievements", "BadgesEnabled"}, false}; + true}; const Info RA_UNOFFICIAL_ENABLED{{System::Achievements, "Achievements", "UnofficialEnabled"}, false}; const Info RA_ENCORE_ENABLED{{System::Achievements, "Achievements", "EncoreEnabled"}, false}; +const Info RA_SPECTATOR_ENABLED{{System::Achievements, "Achievements", "SpectatorEnabled"}, + false}; +const Info RA_PROGRESS_ENABLED{{System::Achievements, "Achievements", "ProgressEnabled"}, + false}; +const Info RA_BADGES_ENABLED{{System::Achievements, "Achievements", "BadgesEnabled"}, false}; } // namespace Config #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h index 2ee420372c..e448054214 100644 --- a/Source/Core/Core/Config/AchievementSettings.h +++ b/Source/Core/Core/Config/AchievementSettings.h @@ -14,14 +14,12 @@ extern const Info RA_ENABLED; extern const Info RA_USERNAME; extern const Info RA_HOST_URL; extern const Info RA_API_TOKEN; -extern const Info RA_ACHIEVEMENTS_ENABLED; -extern const Info RA_LEADERBOARDS_ENABLED; -extern const Info RA_RICH_PRESENCE_ENABLED; extern const Info RA_HARDCORE_ENABLED; -extern const Info RA_PROGRESS_ENABLED; -extern const Info RA_BADGES_ENABLED; extern const Info RA_UNOFFICIAL_ENABLED; extern const Info RA_ENCORE_ENABLED; +extern const Info RA_SPECTATOR_ENABLED; +extern const Info RA_PROGRESS_ENABLED; +extern const Info RA_BADGES_ENABLED; } // namespace Config #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 4adb0a240b..c8f86ef70c 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -171,7 +171,7 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri #ifdef USE_RETRO_ACHIEVEMENTS if (game_id != "00000000") - AchievementManager::GetInstance().SetDisabled(true); + AchievementManager::GetInstance().CloseGame(); #endif // USE_RETRO_ACHIEVEMENTS if (game_id == "00000000") diff --git a/Source/Core/Core/Core.cpp b/Source/Core/Core/Core.cpp index 0dc3db14ce..0498d1ba36 100644 --- a/Source/Core/Core/Core.cpp +++ b/Source/Core/Core/Core.cpp @@ -290,7 +290,6 @@ void Stop(Core::System& system) // - Hammertime! #ifdef USE_RETRO_ACHIEVEMENTS AchievementManager::GetInstance().CloseGame(); - AchievementManager::GetInstance().SetDisabled(false); #endif // USE_RETRO_ACHIEVEMENTS s_is_stopping = true; diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index adf16777ab..fefa5f2d2d 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -399,8 +399,7 @@ void DVDInterface::SetDisc(std::unique_ptr disc, } #ifdef USE_RETRO_ACHIEVEMENTS - AchievementManager::GetInstance().HashGame(disc.get(), - [](AchievementManager::ResponseType r_type) {}); + AchievementManager::GetInstance().LoadGame("", disc.get()); #endif // USE_RETRO_ACHIEVEMENTS // Assume that inserting a disc requires having an empty disc before diff --git a/Source/Core/Core/IOS/ES/ES.cpp b/Source/Core/Core/IOS/ES/ES.cpp index 216821cfbd..8eb35f3db9 100644 --- a/Source/Core/Core/IOS/ES/ES.cpp +++ b/Source/Core/Core/IOS/ES/ES.cpp @@ -481,7 +481,7 @@ bool ESDevice::LaunchPPCTitle(u64 title_id) #ifdef USE_RETRO_ACHIEVEMENTS INFO_LOG_FMT(ACHIEVEMENTS, "WAD and NAND formats not currently supported by Achievement Manager."); - AchievementManager::GetInstance().SetDisabled(true); + AchievementManager::GetInstance().CloseGame(); #endif // USE_RETRO_ACHIEVEMENTS core_timing.RemoveEvent(s_bootstrap_ppc_for_launch_event); diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index 90f07d2756..4b3d0edfd0 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -98,7 +98,7 @@ static size_t s_state_writes_in_queue; static std::condition_variable s_state_write_queue_is_empty; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 167; // Last changed in PR 12494 +constexpr u32 STATE_VERSION = 168; // Last changed in PR 12639 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 @@ -198,6 +198,10 @@ static void DoState(Core::System& system, PointerWrap& p) p.DoMarker("Wiimote"); Gecko::DoState(p); p.DoMarker("Gecko"); + +#ifdef USE_RETRO_ACHIEVEMENTS + AchievementManager::GetInstance().DoState(p); +#endif // USE_RETRO_ACHIEVEMENTS } void LoadFromBuffer(Core::System& system, std::vector& buffer) diff --git a/Source/Core/DolphinQt/Achievements/AchievementBox.cpp b/Source/Core/DolphinQt/Achievements/AchievementBox.cpp new file mode 100644 index 0000000000..534b965d78 --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementBox.cpp @@ -0,0 +1,104 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef USE_RETRO_ACHIEVEMENTS +#include "DolphinQt/Achievements/AchievementBox.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Core/AchievementManager.h" +#include "Core/Config/AchievementSettings.h" + +#include "DolphinQt/QtUtils/FromStdString.h" + +AchievementBox::AchievementBox(QWidget* parent, rc_client_achievement_t* achievement) + : QGroupBox(parent), m_achievement(achievement) +{ + const auto& instance = AchievementManager::GetInstance(); + if (!instance.IsGameLoaded()) + return; + + m_badge = new QLabel(); + QLabel* title = new QLabel(QString::fromUtf8(achievement->title, strlen(achievement->title))); + QLabel* description = + new QLabel(QString::fromUtf8(achievement->description, strlen(achievement->description))); + QLabel* points = new QLabel(tr("%1 points").arg(achievement->points)); + m_status = new QLabel(); + m_progress_bar = new QProgressBar(); + QSizePolicy sp_retain = m_progress_bar->sizePolicy(); + sp_retain.setRetainSizeWhenHidden(true); + m_progress_bar->setSizePolicy(sp_retain); + + QVBoxLayout* a_col_right = new QVBoxLayout(); + a_col_right->addWidget(title); + a_col_right->addWidget(description); + a_col_right->addWidget(points); + a_col_right->addWidget(m_status); + a_col_right->addWidget(m_progress_bar); + QHBoxLayout* a_total = new QHBoxLayout(); + a_total->addWidget(m_badge); + a_total->addLayout(a_col_right); + setLayout(a_total); + + UpdateData(); +} + +void AchievementBox::UpdateData() +{ + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; + + const auto& badge = AchievementManager::GetInstance().GetAchievementBadge( + m_achievement->id, m_achievement->state != RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED); + std::string_view color = AchievementManager::GRAY; + if (m_achievement->unlocked & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) + color = AchievementManager::GOLD; + else if (m_achievement->unlocked & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE) + color = AchievementManager::BLUE; + if (Config::Get(Config::RA_BADGES_ENABLED) && badge.name != "") + { + QImage i_badge{}; + if (i_badge.loadFromData(&badge.badge.front(), static_cast(badge.badge.size()))) + { + m_badge->setPixmap(QPixmap::fromImage(i_badge).scaled(64, 64, Qt::KeepAspectRatio, + Qt::SmoothTransformation)); + m_badge->adjustSize(); + m_badge->setStyleSheet( + QStringLiteral("border: 4px solid %1").arg(QtUtils::FromStdString(color))); + } + } + else + { + m_badge->setText({}); + } + + if (m_achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) + { + m_status->setText( + tr("Unlocked at %1") + .arg(QDateTime::fromSecsSinceEpoch(m_achievement->unlock_time).toString())); + } + else + { + m_status->setText(tr("Locked")); + } + + if (m_achievement->measured_percent > 0.000) + { + m_progress_bar->setRange(0, 100); + m_progress_bar->setValue(m_achievement->measured_percent); + m_progress_bar->setVisible(true); + } + else + { + m_progress_bar->setVisible(false); + } +} + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementBox.h b/Source/Core/DolphinQt/Achievements/AchievementBox.h new file mode 100644 index 0000000000..1bb7ce56ad --- /dev/null +++ b/Source/Core/DolphinQt/Achievements/AchievementBox.h @@ -0,0 +1,32 @@ +// Copyright 2024 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef USE_RETRO_ACHIEVEMENTS +#include + +#include "Core/AchievementManager.h" + +class QLabel; +class QProgressBar; +class QWidget; + +struct rc_api_achievement_definition_t; + +class AchievementBox final : public QGroupBox +{ + Q_OBJECT +public: + explicit AchievementBox(QWidget* parent, rc_client_achievement_t* achievement); + void UpdateData(); + +private: + QLabel* m_badge; + QLabel* m_status; + QProgressBar* m_progress_bar; + + rc_client_achievement_t* m_achievement; +}; + +#endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp index e844d22d39..42b5de602f 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.cpp @@ -11,10 +11,13 @@ #include #include +#include + #include "Core/AchievementManager.h" #include "Core/Config/AchievementSettings.h" #include "Core/Core.h" +#include "DolphinQt/QtUtils/FromStdString.h" #include "DolphinQt/Settings.h" AchievementHeaderWidget::AchievementHeaderWidget(QWidget* parent) : QWidget(parent) @@ -23,21 +26,12 @@ AchievementHeaderWidget::AchievementHeaderWidget(QWidget* parent) : QWidget(pare m_game_icon = new QLabel(); m_name = new QLabel(); m_points = new QLabel(); - m_game_progress_hard = new QProgressBar(); - m_game_progress_soft = new QProgressBar(); + m_game_progress = new QProgressBar(); m_rich_presence = new QLabel(); - m_locked_warning = new QLabel(); - m_locked_warning->setText(tr("Achievements have been disabled.
Please close all running " - "games to re-enable achievements.")); - m_locked_warning->setStyleSheet(QStringLiteral("QLabel { color : red; }")); - - QSizePolicy sp_retain = m_game_progress_hard->sizePolicy(); + QSizePolicy sp_retain = m_game_progress->sizePolicy(); sp_retain.setRetainSizeWhenHidden(true); - m_game_progress_hard->setSizePolicy(sp_retain); - sp_retain = m_game_progress_soft->sizePolicy(); - sp_retain.setRetainSizeWhenHidden(true); - m_game_progress_soft->setSizePolicy(sp_retain); + m_game_progress->setSizePolicy(sp_retain); QVBoxLayout* icon_col = new QVBoxLayout(); icon_col->addWidget(m_user_icon); @@ -45,10 +39,8 @@ AchievementHeaderWidget::AchievementHeaderWidget(QWidget* parent) : QWidget(pare QVBoxLayout* text_col = new QVBoxLayout(); text_col->addWidget(m_name); text_col->addWidget(m_points); - text_col->addWidget(m_game_progress_hard); - text_col->addWidget(m_game_progress_soft); + text_col->addWidget(m_game_progress); text_col->addWidget(m_rich_presence); - text_col->addWidget(m_locked_warning); QHBoxLayout* header_layout = new QHBoxLayout(); header_layout->addLayout(icon_col); header_layout->addLayout(text_col); @@ -61,50 +53,48 @@ AchievementHeaderWidget::AchievementHeaderWidget(QWidget* parent) : QWidget(pare m_total->setContentsMargins(0, 0, 0, 0); m_total->setAlignment(Qt::AlignTop); setLayout(m_total); - - std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; - UpdateData(); } void AchievementHeaderWidget::UpdateData() { + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; auto& instance = AchievementManager::GetInstance(); - if (!instance.IsLoggedIn()) + if (!instance.HasAPIToken()) { m_header_box->setVisible(false); return; } - AchievementManager::PointSpread point_spread = instance.TallyScore(); - QString user_name = QString::fromStdString(instance.GetPlayerDisplayName()); - QString game_name = QString::fromStdString(instance.GetGameDisplayName()); + QString user_name = QtUtils::FromStdString(instance.GetPlayerDisplayName()); + QString game_name = QtUtils::FromStdString(instance.GetGameDisplayName()); AchievementManager::BadgeStatus player_badge = instance.GetPlayerBadge(); AchievementManager::BadgeStatus game_badge = instance.GetGameBadge(); m_user_icon->setVisible(false); m_user_icon->clear(); m_user_icon->setText({}); - if (Config::Get(Config::RA_BADGES_ENABLED)) + if (Config::Get(Config::RA_BADGES_ENABLED) && !player_badge.name.empty()) { - if (!player_badge.name.empty()) + QImage i_user_icon{}; + if (i_user_icon.loadFromData(&player_badge.badge.front(), (int)player_badge.badge.size())) { - QImage i_user_icon{}; - if (i_user_icon.loadFromData(&player_badge.badge.front(), (int)player_badge.badge.size())) - { - m_user_icon->setPixmap(QPixmap::fromImage(i_user_icon) - .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - m_user_icon->adjustSize(); - m_user_icon->setStyleSheet(QStringLiteral("border: 4px solid transparent")); - m_user_icon->setVisible(true); - } + m_user_icon->setPixmap(QPixmap::fromImage(i_user_icon) + .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + m_user_icon->adjustSize(); + m_user_icon->setStyleSheet(QStringLiteral("border: 4px solid transparent")); + m_user_icon->setVisible(true); } } m_game_icon->setVisible(false); m_game_icon->clear(); m_game_icon->setText({}); - if (Config::Get(Config::RA_BADGES_ENABLED)) + + if (instance.IsGameLoaded()) { - if (!game_badge.name.empty()) + rc_client_user_game_summary_t game_summary; + rc_client_get_user_game_summary(instance.GetClient(), &game_summary); + + if (Config::Get(Config::RA_BADGES_ENABLED) && !game_badge.name.empty()) { QImage i_game_icon{}; if (i_game_icon.loadFromData(&game_badge.badge.front(), (int)game_badge.badge.size())) @@ -113,70 +103,39 @@ void AchievementHeaderWidget::UpdateData() .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); m_game_icon->adjustSize(); std::string_view color = AchievementManager::GRAY; - if (point_spread.hard_unlocks == point_spread.total_count) - color = AchievementManager::GOLD; - else if (point_spread.hard_unlocks + point_spread.soft_unlocks == point_spread.total_count) - color = AchievementManager::BLUE; + if (game_summary.num_core_achievements == game_summary.num_unlocked_achievements) + { + color = + instance.IsHardcoreModeActive() ? AchievementManager::GOLD : AchievementManager::BLUE; + } m_game_icon->setStyleSheet( - QStringLiteral("border: 4px solid %1").arg(QString::fromStdString(std::string(color)))); + QStringLiteral("border: 4px solid %1").arg(QtUtils::FromStdString(color))); m_game_icon->setVisible(true); } } - } - if (!game_name.isEmpty()) - { m_name->setText(tr("%1 is playing %2").arg(user_name).arg(game_name)); - m_points->setText(GetPointsString(user_name, point_spread)); + m_points->setText(tr("%1 has unlocked %2/%3 achievements worth %4/%5 points") + .arg(user_name) + .arg(game_summary.num_unlocked_achievements) + .arg(game_summary.num_core_achievements) + .arg(game_summary.points_unlocked) + .arg(game_summary.points_core)); - m_game_progress_hard->setRange(0, point_spread.total_count); - if (!m_game_progress_hard->isVisible()) - m_game_progress_hard->setVisible(true); - m_game_progress_hard->setValue(point_spread.hard_unlocks); - m_game_progress_soft->setRange(0, point_spread.total_count); - m_game_progress_soft->setValue(point_spread.hard_unlocks + point_spread.soft_unlocks); - if (!m_game_progress_soft->isVisible()) - m_game_progress_soft->setVisible(true); + m_game_progress->setRange(0, game_summary.num_core_achievements); + if (!m_game_progress->isVisible()) + m_game_progress->setVisible(true); + m_game_progress->setValue(game_summary.num_unlocked_achievements); m_rich_presence->setText(QString::fromUtf8(instance.GetRichPresence().data())); - if (!m_rich_presence->isVisible()) - m_rich_presence->setVisible(Config::Get(Config::RA_RICH_PRESENCE_ENABLED)); - m_locked_warning->setVisible(false); + m_rich_presence->setVisible(true); } else { m_name->setText(user_name); m_points->setText(tr("%1 points").arg(instance.GetPlayerScore())); - m_game_progress_hard->setVisible(false); - m_game_progress_soft->setVisible(false); + m_game_progress->setVisible(false); m_rich_presence->setVisible(false); - m_locked_warning->setVisible(instance.IsDisabled()); - } -} - -QString -AchievementHeaderWidget::GetPointsString(const QString& user_name, - const AchievementManager::PointSpread& point_spread) const -{ - if (point_spread.soft_points > 0) - { - return tr("%1 has unlocked %2/%3 achievements (%4 hardcore) worth %5/%6 points (%7 hardcore)") - .arg(user_name) - .arg(point_spread.hard_unlocks + point_spread.soft_unlocks) - .arg(point_spread.total_count) - .arg(point_spread.hard_unlocks) - .arg(point_spread.hard_points + point_spread.soft_points) - .arg(point_spread.total_points) - .arg(point_spread.hard_points); - } - else - { - return tr("%1 has unlocked %2/%3 achievements worth %4/%5 points") - .arg(user_name) - .arg(point_spread.hard_unlocks) - .arg(point_spread.total_count) - .arg(point_spread.hard_points) - .arg(point_spread.total_points); } } diff --git a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h index 7a644bd763..0964ef488f 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h +++ b/Source/Core/DolphinQt/Achievements/AchievementHeaderWidget.h @@ -20,17 +20,12 @@ public: void UpdateData(); private: - QString GetPointsString(const QString& user_name, - const AchievementManager::PointSpread& point_spread) const; - QLabel* m_user_icon; QLabel* m_game_icon; QLabel* m_name; QLabel* m_points; - QProgressBar* m_game_progress_hard; - QProgressBar* m_game_progress_soft; + QProgressBar* m_game_progress; QLabel* m_rich_presence; - QLabel* m_locked_warning; QGroupBox* m_header_box; }; diff --git a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp index 13ab205f9c..d2a52970f8 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.cpp @@ -24,11 +24,6 @@ AchievementLeaderboardWidget::AchievementLeaderboardWidget(QWidget* parent) : QW m_common_box = new QGroupBox(); m_common_layout = new QGridLayout(); - { - std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; - UpdateData(); - } - m_common_box->setLayout(m_common_layout); auto* layout = new QVBoxLayout; @@ -38,77 +33,126 @@ AchievementLeaderboardWidget::AchievementLeaderboardWidget(QWidget* parent) : QW setLayout(layout); } -void AchievementLeaderboardWidget::UpdateData() +void AchievementLeaderboardWidget::UpdateData(bool clean_all) { - ClearLayoutRecursively(m_common_layout); - - if (!AchievementManager::GetInstance().IsGameLoaded()) - return; - const auto& leaderboards = AchievementManager::GetInstance().GetLeaderboardsInfo(); - int row = 0; - for (const auto& board_row : leaderboards) + if (clean_all) { - const AchievementManager::LeaderboardStatus& board = board_row.second; - QLabel* a_title = new QLabel(QString::fromStdString(board.name)); - QLabel* a_description = new QLabel(QString::fromStdString(board.description)); - QVBoxLayout* a_col_left = new QVBoxLayout(); - a_col_left->addWidget(a_title); - a_col_left->addWidget(a_description); - if (row > 0) + ClearLayoutRecursively(m_common_layout); + + auto& instance = AchievementManager::GetInstance(); + if (!instance.IsGameLoaded()) + return; + auto* client = instance.GetClient(); + auto* leaderboard_list = + rc_client_create_leaderboard_list(client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE); + + u32 row = 0; + for (u32 bucket = 0; bucket < leaderboard_list->num_buckets; bucket++) { - QFrame* a_divider = new QFrame(); - a_divider->setFrameShape(QFrame::HLine); - m_common_layout->addWidget(a_divider, row - 1, 0); - } - m_common_layout->addLayout(a_col_left, row, 0); - // Each leaderboard entry is displayed with four values. These are *generally* intended to be, - // in order, the first place entry, the entry one above the player, the player's entry, and - // the entry one below the player. - // Edge cases: - // * If there are fewer than four entries in the leaderboard, all entries will be shown in - // order and the remainder of the list will be padded with empty values. - // * If the player does not currently have a score in the leaderboard, or is in the top 3, - // the four slots will be the top four players in order. - // * If the player is last place, the player will be in the fourth slot, and the second and - // third slots will be the two players above them. The first slot will always be first place. - std::array to_display{1, 2, 3, 4}; - if (board.player_index > to_display.size() - 1) - { - // If the rank one below than the player is found, offset = 1. - u32 offset = static_cast(board.entries.count(board.player_index + 1)); - // Example: player is 10th place but not last - // to_display = {1, 10-3+1+1, 10-3+1+2, 10-3+1+3} = {1, 9, 10, 11} - // Example: player is 15th place and is last - // to_display = {1, 15-3+0+1, 15-3+0+2, 15-3+0+3} = {1, 13, 14, 15} - for (size_t i = 1; i < to_display.size(); ++i) - to_display[i] = board.player_index - 3 + offset + static_cast(i); - } - for (size_t i = 0; i < to_display.size(); ++i) - { - u32 index = to_display[i]; - QLabel* a_rank = new QLabel(QStringLiteral("---")); - QLabel* a_username = new QLabel(QStringLiteral("---")); - QLabel* a_score = new QLabel(QStringLiteral("---")); - const auto it = board.entries.find(index); - if (it != board.entries.end()) + const auto& leaderboard_bucket = leaderboard_list->buckets[bucket]; + for (u32 board = 0; board < leaderboard_bucket.num_leaderboards; board++) { - a_rank->setText(tr("Rank %1").arg(it->second.rank)); - a_username->setText(QString::fromStdString(it->second.username)); - a_score->setText(QString::fromUtf8(it->second.score.data())); + const auto* leaderboard = leaderboard_bucket.leaderboards[board]; + m_leaderboard_order[leaderboard->id] = row; + QLabel* a_title = new QLabel(QString::fromUtf8(leaderboard->title)); + QLabel* a_description = new QLabel(QString::fromUtf8(leaderboard->description)); + QVBoxLayout* a_col_left = new QVBoxLayout(); + a_col_left->addWidget(a_title); + a_col_left->addWidget(a_description); + if (row > 0) + { + QFrame* a_divider = new QFrame(); + a_divider->setFrameShape(QFrame::HLine); + m_common_layout->addWidget(a_divider, row - 1, 0); + } + m_common_layout->addLayout(a_col_left, row, 0); + for (size_t ix = 0; ix < 4; ix++) + { + QVBoxLayout* a_col = new QVBoxLayout(); + for (size_t jx = 0; jx < 3; jx++) + a_col->addWidget(new QLabel(QStringLiteral("---"))); + if (row > 0) + { + QFrame* a_divider = new QFrame(); + a_divider->setFrameShape(QFrame::HLine); + m_common_layout->addWidget(a_divider, row - 1, static_cast(ix) + 1); + } + m_common_layout->addLayout(a_col, row, static_cast(ix) + 1); + } + row += 2; } + } + rc_client_destroy_leaderboard_list(leaderboard_list); + } + for (auto row : m_leaderboard_order) + { + UpdateRow(row.second); + } +} + +void AchievementLeaderboardWidget::UpdateData( + const std::set& update_ids) +{ + for (auto row : m_leaderboard_order) + { + if (update_ids.contains(row.first)) + { + UpdateRow(row.second); + } + } +} + +void AchievementLeaderboardWidget::UpdateRow(AchievementManager::AchievementId leaderboard_id) +{ + const auto leaderboard_itr = m_leaderboard_order.find(leaderboard_id); + if (leaderboard_itr == m_leaderboard_order.end()) + return; + const int row = leaderboard_itr->second; + + const AchievementManager::LeaderboardStatus* board; + { + std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; + board = AchievementManager::GetInstance().GetLeaderboardInfo(leaderboard_id); + } + if (!board) + return; + + // Each leaderboard entry is displayed with four values. These are *generally* intended to be, + // in order, the first place entry, the entry one above the player, the player's entry, and + // the entry one below the player. + // Edge cases: + // * If there are fewer than four entries in the leaderboard, all entries will be shown in + // order and the remainder of the list will be padded with empty values. + // * If the player does not currently have a score in the leaderboard, or is in the top 3, + // the four slots will be the top four players in order. + // * If the player is last place, the player will be in the fourth slot, and the second and + // third slots will be the two players above them. The first slot will always be first place. + std::array to_display{1, 2, 3, 4}; + if (board->player_index > to_display.size() - 1) + { + // If the rank one below than the player is found, offset = 1. + u32 offset = static_cast(board->entries.count(board->player_index + 1)); + // Example: player is 10th place but not last + // to_display = {1, 10-3+1+1, 10-3+1+2, 10-3+1+3} = {1, 9, 10, 11} + // Example: player is 15th place and is last + // to_display = {1, 15-3+0+1, 15-3+0+2, 15-3+0+3} = {1, 13, 14, 15} + for (size_t ix = 1; ix < to_display.size(); ++ix) + to_display[ix] = board->player_index - 3 + offset + static_cast(ix); + } + for (size_t ix = 0; ix < to_display.size(); ++ix) + { + const auto it = board->entries.find(to_display[ix]); + if (it != board->entries.end()) + { QVBoxLayout* a_col = new QVBoxLayout(); - a_col->addWidget(a_rank); - a_col->addWidget(a_username); - a_col->addWidget(a_score); - if (row > 0) - { - QFrame* a_divider = new QFrame(); - a_divider->setFrameShape(QFrame::HLine); - m_common_layout->addWidget(a_divider, row - 1, static_cast(i) + 1); - } - m_common_layout->addLayout(a_col, row, static_cast(i) + 1); + a_col->addWidget(new QLabel(tr("Rank %1").arg(it->second.rank))); + a_col->addWidget(new QLabel(QString::fromStdString(it->second.username))); + a_col->addWidget(new QLabel(QString::fromUtf8(it->second.score.data()))); + auto old_item = m_common_layout->itemAtPosition(row, static_cast(ix) + 1); + m_common_layout->removeItem(old_item); + ClearLayoutRecursively(static_cast(old_item)); + m_common_layout->addLayout(a_col, row, static_cast(ix) + 1); } - row += 2; } } diff --git a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h index 055ea6ab3f..cafd2483bd 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h +++ b/Source/Core/DolphinQt/Achievements/AchievementLeaderboardWidget.h @@ -6,6 +6,8 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include +#include "Core/AchievementManager.h" + class QGroupBox; class QGridLayout; @@ -14,11 +16,14 @@ class AchievementLeaderboardWidget final : public QWidget Q_OBJECT public: explicit AchievementLeaderboardWidget(QWidget* parent); - void UpdateData(); + void UpdateData(bool clean_all); + void UpdateData(const std::set& update_ids); + void UpdateRow(AchievementManager::AchievementId leaderboard_id); private: QGroupBox* m_common_box; QGridLayout* m_common_layout; + std::map m_leaderboard_order; }; #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp index 39cc36948c..0164ee6a78 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.cpp @@ -18,21 +18,15 @@ #include "Core/Config/MainSettings.h" #include "Core/Core.h" +#include "DolphinQt/Achievements/AchievementBox.h" #include "DolphinQt/QtUtils/ClearLayoutRecursively.h" #include "DolphinQt/Settings.h" -static constexpr bool hardcore_mode_enabled = false; - AchievementProgressWidget::AchievementProgressWidget(QWidget* parent) : QWidget(parent) { m_common_box = new QGroupBox(); m_common_layout = new QVBoxLayout(); - { - std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; - UpdateData(); - } - m_common_box->setLayout(m_common_layout); auto* layout = new QVBoxLayout; @@ -42,124 +36,50 @@ AchievementProgressWidget::AchievementProgressWidget(QWidget* parent) : QWidget( setLayout(layout); } -QGroupBox* -AchievementProgressWidget::CreateAchievementBox(const rc_api_achievement_definition_t* achievement) +void AchievementProgressWidget::UpdateData(bool clean_all) { - const auto& instance = AchievementManager::GetInstance(); - if (!instance.IsGameLoaded()) - return new QGroupBox(); + if (clean_all) + { + m_achievement_boxes.clear(); + ClearLayoutRecursively(m_common_layout); - QLabel* a_badge = new QLabel(); - const auto unlock_status = instance.GetUnlockStatus(achievement->id); - const AchievementManager::BadgeStatus* badge = &unlock_status.locked_badge; - std::string_view color = AchievementManager::GRAY; - if (unlock_status.remote_unlock_status == AchievementManager::UnlockStatus::UnlockType::HARDCORE) - { - badge = &unlock_status.unlocked_badge; - color = AchievementManager::GOLD; - } - else if (hardcore_mode_enabled && unlock_status.session_unlock_count > 1) - { - badge = &unlock_status.unlocked_badge; - color = AchievementManager::GOLD; - } - else if (unlock_status.remote_unlock_status == - AchievementManager::UnlockStatus::UnlockType::SOFTCORE) - { - badge = &unlock_status.unlocked_badge; - color = AchievementManager::BLUE; - } - else if (unlock_status.session_unlock_count > 1) - { - badge = &unlock_status.unlocked_badge; - color = AchievementManager::BLUE; - } - if (Config::Get(Config::RA_BADGES_ENABLED) && badge->name != "") - { - QImage i_badge{}; - if (i_badge.loadFromData(&badge->badge.front(), (int)badge->badge.size())) + auto& instance = AchievementManager::GetInstance(); + if (!instance.IsGameLoaded()) + return; + auto* client = instance.GetClient(); + auto* achievement_list = rc_client_create_achievement_list( + client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); + for (u32 ix = 0; ix < achievement_list->num_buckets; ix++) { - a_badge->setPixmap(QPixmap::fromImage(i_badge).scaled(64, 64, Qt::KeepAspectRatio, - Qt::SmoothTransformation)); - a_badge->adjustSize(); - a_badge->setStyleSheet( - QStringLiteral("border: 4px solid %1").arg(QString::fromStdString(std::string(color)))); + for (u32 jx = 0; jx < achievement_list->buckets[ix].num_achievements; jx++) + { + auto* achievement = achievement_list->buckets[ix].achievements[jx]; + m_achievement_boxes[achievement->id] = std::make_shared(this, achievement); + m_common_layout->addWidget(m_achievement_boxes[achievement->id].get()); + } } - } - - QLabel* a_title = new QLabel(QString::fromUtf8(achievement->title, strlen(achievement->title))); - QLabel* a_description = - new QLabel(QString::fromUtf8(achievement->description, strlen(achievement->description))); - QLabel* a_points = new QLabel(tr("%1 points").arg(achievement->points)); - QLabel* a_status = new QLabel(GetStatusString(achievement->id)); - QProgressBar* a_progress_bar = new QProgressBar(); - QSizePolicy sp_retain = a_progress_bar->sizePolicy(); - sp_retain.setRetainSizeWhenHidden(true); - a_progress_bar->setSizePolicy(sp_retain); - unsigned int value = 0; - unsigned int target = 0; - if (AchievementManager::GetInstance().GetAchievementProgress(achievement->id, &value, &target) == - AchievementManager::ResponseType::SUCCESS && - target > 0) - { - a_progress_bar->setRange(0, target); - a_progress_bar->setValue(value); + rc_client_destroy_achievement_list(achievement_list); } else { - a_progress_bar->setVisible(false); - } - - QVBoxLayout* a_col_right = new QVBoxLayout(); - a_col_right->addWidget(a_title); - a_col_right->addWidget(a_description); - a_col_right->addWidget(a_points); - a_col_right->addWidget(a_status); - a_col_right->addWidget(a_progress_bar); - QHBoxLayout* a_total = new QHBoxLayout(); - a_total->addWidget(a_badge); - a_total->addLayout(a_col_right); - QGroupBox* a_group_box = new QGroupBox(); - a_group_box->setLayout(a_total); - return a_group_box; -} - -void AchievementProgressWidget::UpdateData() -{ - ClearLayoutRecursively(m_common_layout); - - auto& instance = AchievementManager::GetInstance(); - if (!instance.IsGameLoaded()) - return; - - const auto* game_data = instance.GetGameData(); - for (u32 ix = 0; ix < game_data->num_achievements; ix++) - { - m_common_layout->addWidget(CreateAchievementBox(game_data->achievements + ix)); - } -} - -QString AchievementProgressWidget::GetStatusString(u32 achievement_id) const -{ - const auto unlock_status = AchievementManager::GetInstance().GetUnlockStatus(achievement_id); - if (unlock_status.session_unlock_count > 0) - { - if (Config::Get(Config::RA_ENCORE_ENABLED)) + for (auto box : m_achievement_boxes) { - return tr("Unlocked %1 times this session").arg(unlock_status.session_unlock_count); + box.second->UpdateData(); } - return tr("Unlocked this session"); } - switch (unlock_status.remote_unlock_status) +} + +void AchievementProgressWidget::UpdateData( + const std::set& update_ids) +{ + for (auto& [id, box] : m_achievement_boxes) { - case AchievementManager::UnlockStatus::UnlockType::LOCKED: - return tr("Locked"); - case AchievementManager::UnlockStatus::UnlockType::SOFTCORE: - return tr("Unlocked (Casual)"); - case AchievementManager::UnlockStatus::UnlockType::HARDCORE: - return tr("Unlocked"); + if (update_ids.contains(id)) + { + box->UpdateData(); + } } - return {}; } #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h index b1e09e40d8..9aa2f8cfe7 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h +++ b/Source/Core/DolphinQt/Achievements/AchievementProgressWidget.h @@ -7,7 +7,9 @@ #include #include "Common/CommonTypes.h" +#include "Core/AchievementManager.h" +class AchievementBox; class QCheckBox; class QGroupBox; class QLineEdit; @@ -21,14 +23,13 @@ class AchievementProgressWidget final : public QWidget Q_OBJECT public: explicit AchievementProgressWidget(QWidget* parent); - void UpdateData(); + void UpdateData(bool clean_all); + void UpdateData(const std::set& update_ids); private: - QGroupBox* CreateAchievementBox(const rc_api_achievement_definition_t* achievement); - QString GetStatusString(u32 achievement_id) const; - QGroupBox* m_common_box; QVBoxLayout* m_common_layout; + std::map> m_achievement_boxes; }; #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp index 07ec6fb78f..cc6a4c1c50 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.cpp @@ -61,24 +61,6 @@ void AchievementSettingsWidget::CreateLayout() m_common_login_failed = new QLabel(tr("Login Failed")); m_common_login_failed->setStyleSheet(QStringLiteral("QLabel { color : red; }")); m_common_login_failed->setVisible(false); - m_common_achievements_enabled_input = new ToolTipCheckBox(tr("Enable Achievements")); - m_common_achievements_enabled_input->SetDescription(tr("Enable unlocking achievements.
")); - m_common_leaderboards_enabled_input = new ToolTipCheckBox(tr("Enable Leaderboards")); - m_common_leaderboards_enabled_input->SetDescription( - tr("Enable competing in RetroAchievements leaderboards.

Hardcore Mode must be enabled " - "to use.")); - m_common_rich_presence_enabled_input = new ToolTipCheckBox(tr("Enable Rich Presence")); - m_common_rich_presence_enabled_input->SetDescription( - tr("Enable detailed rich presence on the RetroAchievements website.

This provides a " - "detailed description of what the player is doing in game to the website. If this is " - "disabled, the website will only report what game is being played.

This has no " - "bearing on Discord rich presence.")); - m_common_unofficial_enabled_input = new ToolTipCheckBox(tr("Enable Unofficial Achievements")); - m_common_unofficial_enabled_input->SetDescription( - tr("Enable unlocking unofficial achievements as well as official " - "achievements.

Unofficial achievements may be optional or unfinished achievements " - "that have not been deemed official by RetroAchievements and may be useful for testing or " - "simply for fun.")); m_common_hardcore_enabled_input = new ToolTipCheckBox(tr("Enable Hardcore Mode")); m_common_hardcore_enabled_input->SetDescription( tr("Enable Hardcore Mode on RetroAchievements.

Hardcore Mode is intended to provide " @@ -93,6 +75,25 @@ void AchievementSettingsWidget::CreateLayout() "playing.
Close your current game before enabling.
Be aware that " "turning Hardcore Mode off while a game is running requires the game to be closed before " "re-enabling.")); + m_common_unofficial_enabled_input = new ToolTipCheckBox(tr("Enable Unofficial Achievements")); + m_common_unofficial_enabled_input->SetDescription( + tr("Enable unlocking unofficial achievements as well as official " + "achievements.

Unofficial achievements may be optional or unfinished achievements " + "that have not been deemed official by RetroAchievements and may be useful for testing or " + "simply for fun.

Setting takes effect on next game load.")); + m_common_encore_enabled_input = new ToolTipCheckBox(tr("Enable Encore Achievements")); + m_common_encore_enabled_input->SetDescription( + tr("Enable unlocking achievements in Encore Mode.

Encore Mode re-enables achievements " + "the player has already unlocked on the site so that the player will be notified if they " + "meet the unlock conditions again, useful for custom speedrun criteria or simply for fun." + "

Setting takes effect on next game load.")); + m_common_spectator_enabled_input = new ToolTipCheckBox(tr("Enable Spectator Mode")); + m_common_spectator_enabled_input->SetDescription( + tr("Enable unlocking achievements in Spectator Mode.

While in Spectator Mode, " + "achievements and leaderboards will be processed and displayed on screen, but will not be " + "submitted to the server.

If this is on at game launch, it will not be turned off " + "until game close, because a RetroAchievements session will not be created.

If " + "this is off at game launch, it can be toggled freely while the game is running.")); m_common_progress_enabled_input = new ToolTipCheckBox(tr("Enable Progress Notifications")); m_common_progress_enabled_input->SetDescription( tr("Enable progress notifications on achievements.

Displays a brief popup message " @@ -103,11 +104,6 @@ void AchievementSettingsWidget::CreateLayout() tr("Enable achievement badges.

Displays icons for the player, game, and achievements. " "Simple visual option, but will require a small amount of extra memory and time to " "download the images.")); - m_common_encore_enabled_input = new ToolTipCheckBox(tr("Enable Encore Achievements")); - m_common_encore_enabled_input->SetDescription(tr( - "Enable unlocking achievements in Encore Mode.

Encore Mode re-enables achievements " - "the player has already unlocked on the site so that the player will be notified if they " - "meet the unlock conditions again, useful for custom speedrun criteria or simply for fun.")); m_common_layout->addWidget(m_common_integration_enabled_input); m_common_layout->addWidget(m_common_username_label); @@ -117,14 +113,14 @@ void AchievementSettingsWidget::CreateLayout() m_common_layout->addWidget(m_common_login_button); m_common_layout->addWidget(m_common_logout_button); m_common_layout->addWidget(m_common_login_failed); - m_common_layout->addWidget(m_common_achievements_enabled_input); - m_common_layout->addWidget(m_common_leaderboards_enabled_input); - m_common_layout->addWidget(m_common_rich_presence_enabled_input); + m_common_layout->addWidget(new QLabel(tr("Function Settings"))); m_common_layout->addWidget(m_common_hardcore_enabled_input); - m_common_layout->addWidget(m_common_progress_enabled_input); - m_common_layout->addWidget(m_common_badges_enabled_input); m_common_layout->addWidget(m_common_unofficial_enabled_input); m_common_layout->addWidget(m_common_encore_enabled_input); + m_common_layout->addWidget(m_common_spectator_enabled_input); + m_common_layout->addWidget(new QLabel(tr("Display Settings"))); + m_common_layout->addWidget(m_common_progress_enabled_input); + m_common_layout->addWidget(m_common_badges_enabled_input); m_common_layout->setAlignment(Qt::AlignTop); setLayout(m_common_layout); @@ -136,22 +132,18 @@ void AchievementSettingsWidget::ConnectWidgets() &AchievementSettingsWidget::ToggleRAIntegration); connect(m_common_login_button, &QPushButton::pressed, this, &AchievementSettingsWidget::Login); connect(m_common_logout_button, &QPushButton::pressed, this, &AchievementSettingsWidget::Logout); - connect(m_common_achievements_enabled_input, &QCheckBox::toggled, this, - &AchievementSettingsWidget::ToggleAchievements); - connect(m_common_leaderboards_enabled_input, &QCheckBox::toggled, this, - &AchievementSettingsWidget::ToggleLeaderboards); - connect(m_common_rich_presence_enabled_input, &QCheckBox::toggled, this, - &AchievementSettingsWidget::ToggleRichPresence); connect(m_common_hardcore_enabled_input, &QCheckBox::toggled, this, &AchievementSettingsWidget::ToggleHardcore); - connect(m_common_progress_enabled_input, &QCheckBox::toggled, this, - &AchievementSettingsWidget::ToggleProgress); - connect(m_common_badges_enabled_input, &QCheckBox::toggled, this, - &AchievementSettingsWidget::ToggleBadges); connect(m_common_unofficial_enabled_input, &QCheckBox::toggled, this, &AchievementSettingsWidget::ToggleUnofficial); connect(m_common_encore_enabled_input, &QCheckBox::toggled, this, &AchievementSettingsWidget::ToggleEncore); + connect(m_common_spectator_enabled_input, &QCheckBox::toggled, this, + &AchievementSettingsWidget::ToggleSpectator); + connect(m_common_progress_enabled_input, &QCheckBox::toggled, this, + &AchievementSettingsWidget::ToggleProgress); + connect(m_common_badges_enabled_input, &QCheckBox::toggled, this, + &AchievementSettingsWidget::ToggleBadges); } void AchievementSettingsWidget::OnControllerInterfaceConfigure() @@ -165,7 +157,6 @@ void AchievementSettingsWidget::OnControllerInterfaceConfigure() void AchievementSettingsWidget::LoadSettings() { bool enabled = Config::Get(Config::RA_ENABLED); - bool achievements_enabled = Config::Get(Config::RA_ACHIEVEMENTS_ENABLED); bool hardcore_enabled = Config::Get(Config::RA_HARDCORE_ENABLED); bool logged_out = Config::Get(Config::RA_API_TOKEN).empty(); std::string username = Config::Get(Config::RA_USERNAME); @@ -184,17 +175,6 @@ void AchievementSettingsWidget::LoadSettings() SignalBlocking(m_common_logout_button)->setVisible(!logged_out); SignalBlocking(m_common_logout_button)->setEnabled(enabled); - SignalBlocking(m_common_achievements_enabled_input)->setChecked(achievements_enabled); - SignalBlocking(m_common_achievements_enabled_input)->setEnabled(enabled); - - SignalBlocking(m_common_leaderboards_enabled_input) - ->setChecked(Config::Get(Config::RA_LEADERBOARDS_ENABLED)); - SignalBlocking(m_common_leaderboards_enabled_input)->setEnabled(enabled && hardcore_enabled); - - SignalBlocking(m_common_rich_presence_enabled_input) - ->setChecked(Config::Get(Config::RA_RICH_PRESENCE_ENABLED)); - SignalBlocking(m_common_rich_presence_enabled_input)->setEnabled(enabled); - SignalBlocking(m_common_hardcore_enabled_input) ->setChecked(Config::Get(Config::RA_HARDCORE_ENABLED)); auto& system = Core::System::GetInstance(); @@ -203,19 +183,23 @@ void AchievementSettingsWidget::LoadSettings() (hardcore_enabled || (Core::GetState(system) == Core::State::Uninitialized && !system.GetMovie().IsPlayingInput()))); + SignalBlocking(m_common_unofficial_enabled_input) + ->setChecked(Config::Get(Config::RA_UNOFFICIAL_ENABLED)); + SignalBlocking(m_common_unofficial_enabled_input)->setEnabled(enabled); + + SignalBlocking(m_common_encore_enabled_input)->setChecked(Config::Get(Config::RA_ENCORE_ENABLED)); + SignalBlocking(m_common_encore_enabled_input)->setEnabled(enabled); + + SignalBlocking(m_common_spectator_enabled_input) + ->setChecked(Config::Get(Config::RA_SPECTATOR_ENABLED)); + SignalBlocking(m_common_spectator_enabled_input)->setEnabled(enabled); + SignalBlocking(m_common_progress_enabled_input) ->setChecked(Config::Get(Config::RA_PROGRESS_ENABLED)); - SignalBlocking(m_common_progress_enabled_input)->setEnabled(enabled && achievements_enabled); + SignalBlocking(m_common_progress_enabled_input)->setEnabled(enabled); SignalBlocking(m_common_badges_enabled_input)->setChecked(Config::Get(Config::RA_BADGES_ENABLED)); SignalBlocking(m_common_badges_enabled_input)->setEnabled(enabled); - - SignalBlocking(m_common_unofficial_enabled_input) - ->setChecked(Config::Get(Config::RA_UNOFFICIAL_ENABLED)); - SignalBlocking(m_common_unofficial_enabled_input)->setEnabled(enabled && achievements_enabled); - - SignalBlocking(m_common_encore_enabled_input)->setChecked(Config::Get(Config::RA_ENCORE_ENABLED)); - SignalBlocking(m_common_encore_enabled_input)->setEnabled(enabled && achievements_enabled); } void AchievementSettingsWidget::SaveSettings() @@ -223,20 +207,16 @@ void AchievementSettingsWidget::SaveSettings() Config::ConfigChangeCallbackGuard config_guard; Config::SetBaseOrCurrent(Config::RA_ENABLED, m_common_integration_enabled_input->isChecked()); - Config::SetBaseOrCurrent(Config::RA_ACHIEVEMENTS_ENABLED, - m_common_achievements_enabled_input->isChecked()); - Config::SetBaseOrCurrent(Config::RA_LEADERBOARDS_ENABLED, - m_common_leaderboards_enabled_input->isChecked()); - Config::SetBaseOrCurrent(Config::RA_RICH_PRESENCE_ENABLED, - m_common_rich_presence_enabled_input->isChecked()); Config::SetBaseOrCurrent(Config::RA_HARDCORE_ENABLED, m_common_hardcore_enabled_input->isChecked()); - Config::SetBaseOrCurrent(Config::RA_PROGRESS_ENABLED, - m_common_unofficial_enabled_input->isChecked()); - Config::SetBaseOrCurrent(Config::RA_BADGES_ENABLED, m_common_badges_enabled_input->isChecked()); Config::SetBaseOrCurrent(Config::RA_UNOFFICIAL_ENABLED, m_common_unofficial_enabled_input->isChecked()); Config::SetBaseOrCurrent(Config::RA_ENCORE_ENABLED, m_common_encore_enabled_input->isChecked()); + Config::SetBaseOrCurrent(Config::RA_SPECTATOR_ENABLED, + m_common_spectator_enabled_input->isChecked()); + Config::SetBaseOrCurrent(Config::RA_PROGRESS_ENABLED, + m_common_progress_enabled_input->isChecked()); + Config::SetBaseOrCurrent(Config::RA_BADGES_ENABLED, m_common_badges_enabled_input->isChecked()); Config::Save(); } @@ -256,7 +236,6 @@ void AchievementSettingsWidget::Login() Config::SetBaseOrCurrent(Config::RA_USERNAME, m_common_username_input->text().toStdString()); AchievementManager::GetInstance().Login(m_common_password_input->text().toStdString()); m_common_password_input->setText(QString()); - m_common_login_failed->setVisible(Config::Get(Config::RA_API_TOKEN).empty()); SaveSettings(); } @@ -266,27 +245,10 @@ void AchievementSettingsWidget::Logout() SaveSettings(); } -void AchievementSettingsWidget::ToggleAchievements() -{ - SaveSettings(); - AchievementManager::GetInstance().ActivateDeactivateAchievements(); -} - -void AchievementSettingsWidget::ToggleLeaderboards() -{ - SaveSettings(); - AchievementManager::GetInstance().ActivateDeactivateLeaderboards(); -} - -void AchievementSettingsWidget::ToggleRichPresence() -{ - SaveSettings(); - AchievementManager::GetInstance().ActivateDeactivateRichPresence(); -} - void AchievementSettingsWidget::ToggleHardcore() { SaveSettings(); + AchievementManager::GetInstance().SetHardcoreMode(); if (Config::Get(Config::RA_HARDCORE_ENABLED)) { if (Config::Get(Config::MAIN_EMULATION_SPEED) < 1.0f) @@ -298,6 +260,22 @@ void AchievementSettingsWidget::ToggleHardcore() emit Settings::Instance().EmulationStateChanged(Core::GetState(Core::System::GetInstance())); } +void AchievementSettingsWidget::ToggleUnofficial() +{ + SaveSettings(); +} + +void AchievementSettingsWidget::ToggleEncore() +{ + SaveSettings(); +} + +void AchievementSettingsWidget::ToggleSpectator() +{ + SaveSettings(); + AchievementManager::GetInstance().SetSpectatorMode(); +} + void AchievementSettingsWidget::ToggleProgress() { SaveSettings(); @@ -306,19 +284,8 @@ void AchievementSettingsWidget::ToggleProgress() void AchievementSettingsWidget::ToggleBadges() { SaveSettings(); - AchievementManager::GetInstance().FetchBadges(); -} - -void AchievementSettingsWidget::ToggleUnofficial() -{ - SaveSettings(); - AchievementManager::GetInstance().ActivateDeactivateAchievements(); -} - -void AchievementSettingsWidget::ToggleEncore() -{ - SaveSettings(); - AchievementManager::GetInstance().ActivateDeactivateAchievements(); + AchievementManager::GetInstance().FetchPlayerBadge(); + AchievementManager::GetInstance().FetchGameBadges(); } #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h index eb3f237389..68ba658b8f 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h +++ b/Source/Core/DolphinQt/Achievements/AchievementSettingsWidget.h @@ -32,14 +32,12 @@ private: void ToggleRAIntegration(); void Login(); void Logout(); - void ToggleAchievements(); - void ToggleLeaderboards(); - void ToggleRichPresence(); void ToggleHardcore(); - void ToggleProgress(); - void ToggleBadges(); void ToggleUnofficial(); void ToggleEncore(); + void ToggleSpectator(); + void ToggleProgress(); + void ToggleBadges(); QGroupBox* m_common_box; QVBoxLayout* m_common_layout; @@ -51,14 +49,12 @@ private: QLineEdit* m_common_password_input; QPushButton* m_common_login_button; QPushButton* m_common_logout_button; - ToolTipCheckBox* m_common_achievements_enabled_input; - ToolTipCheckBox* m_common_leaderboards_enabled_input; - ToolTipCheckBox* m_common_rich_presence_enabled_input; ToolTipCheckBox* m_common_hardcore_enabled_input; - ToolTipCheckBox* m_common_progress_enabled_input; - ToolTipCheckBox* m_common_badges_enabled_input; ToolTipCheckBox* m_common_unofficial_enabled_input; ToolTipCheckBox* m_common_encore_enabled_input; + ToolTipCheckBox* m_common_spectator_enabled_input; + ToolTipCheckBox* m_common_progress_enabled_input; + ToolTipCheckBox* m_common_badges_enabled_input; }; #endif // USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp index 3704884a2b..b796b9dc3e 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.cpp @@ -28,11 +28,13 @@ AchievementsWindow::AchievementsWindow(QWidget* parent) : QDialog(parent) CreateMainLayout(); ConnectWidgets(); AchievementManager::GetInstance().SetUpdateCallback( - [this] { QueueOnObject(this, &AchievementsWindow::UpdateData); }); + [this](AchievementManager::UpdatedItems updated_items) { + QueueOnObject(this, [this, updated_items = std::move(updated_items)] { + AchievementsWindow::UpdateData(std::move(updated_items)); + }); + }); connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, - &AchievementsWindow::UpdateData); - - UpdateData(); + [this] { AchievementsWindow::UpdateData({.all = true}); }); } void AchievementsWindow::showEvent(QShowEvent* event) @@ -71,19 +73,38 @@ void AchievementsWindow::ConnectWidgets() connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); } -void AchievementsWindow::UpdateData() +void AchievementsWindow::UpdateData(AchievementManager::UpdatedItems updated_items) { + m_settings_widget->UpdateData(); + if (updated_items.all) + { + m_header_widget->UpdateData(); + m_progress_widget->UpdateData(true); + m_leaderboard_widget->UpdateData(true); + } + else + { + if (updated_items.player_icon || updated_items.game_icon || updated_items.rich_presence || + updated_items.all_achievements || updated_items.achievements.size() > 0) + { + m_header_widget->UpdateData(); + } + if (updated_items.all_achievements) + m_progress_widget->UpdateData(false); + else if (updated_items.achievements.size() > 0) + m_progress_widget->UpdateData(updated_items.achievements); + if (updated_items.all_leaderboards) + m_leaderboard_widget->UpdateData(false); + else if (updated_items.leaderboards.size() > 0) + m_leaderboard_widget->UpdateData(updated_items.leaderboards); + } + { auto& instance = AchievementManager::GetInstance(); std::lock_guard lg{instance.GetLock()}; const bool is_game_loaded = instance.IsGameLoaded(); - - m_header_widget->UpdateData(); - m_header_widget->setVisible(instance.IsLoggedIn()); - m_settings_widget->UpdateData(); - m_progress_widget->UpdateData(); + m_header_widget->setVisible(instance.HasAPIToken()); m_tab_widget->setTabVisible(1, is_game_loaded); - m_leaderboard_widget->UpdateData(); m_tab_widget->setTabVisible(2, is_game_loaded); } update(); diff --git a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h index 751749fbc3..3012707b3d 100644 --- a/Source/Core/DolphinQt/Achievements/AchievementsWindow.h +++ b/Source/Core/DolphinQt/Achievements/AchievementsWindow.h @@ -6,6 +6,8 @@ #ifdef USE_RETRO_ACHIEVEMENTS #include +#include "Core/AchievementManager.h" + class AchievementHeaderWidget; class AchievementLeaderboardWidget; class AchievementSettingsWidget; @@ -19,7 +21,7 @@ class AchievementsWindow : public QDialog Q_OBJECT public: explicit AchievementsWindow(QWidget* parent); - void UpdateData(); + void UpdateData(AchievementManager::UpdatedItems updated_items); void ForceSettingsTab(); private: diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 87bd9883d5..dc946b5b50 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -28,6 +28,8 @@ add_executable(dolphin-emu CheatSearchWidget.h CheatsManager.cpp CheatsManager.h + Achievements/AchievementBox.cpp + Achievements/AchievementBox.h Achievements/AchievementHeaderWidget.cpp Achievements/AchievementHeaderWidget.h Achievements/AchievementLeaderboardWidget.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index cbcd07bfad..9dd72e3622 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -48,6 +48,7 @@ + @@ -267,6 +268,7 @@ + diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index aacf4f933b..f1c6951595 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -2005,6 +2005,7 @@ void MainWindow::ShowAchievementsWindow() m_achievements_window->show(); m_achievements_window->raise(); m_achievements_window->activateWindow(); + m_achievements_window->UpdateData(AchievementManager::UpdatedItems{.all = true}); } void MainWindow::ShowAchievementSettings() diff --git a/Source/Core/VideoCommon/OnScreenUI.cpp b/Source/Core/VideoCommon/OnScreenUI.cpp index 4d0213a1a4..715f218b05 100644 --- a/Source/Core/VideoCommon/OnScreenUI.cpp +++ b/Source/Core/VideoCommon/OnScreenUI.cpp @@ -332,63 +332,70 @@ void OnScreenUI::DrawDebugText() } #ifdef USE_RETRO_ACHIEVEMENTS -void OnScreenUI::DrawChallenges() +void OnScreenUI::DrawChallengesAndLeaderboards() { std::lock_guard lg{AchievementManager::GetInstance().GetLock()}; const auto& challenge_icons = AchievementManager::GetInstance().GetChallengeIcons(); - if (challenge_icons.empty()) - return; - - const std::string window_name = "Challenges"; - - u32 sum_of_icon_heights = 0; - u32 max_icon_width = 0; - for (const auto& [name, icon] : challenge_icons) + const auto& leaderboard_progress = AchievementManager::GetInstance().GetActiveLeaderboards(); + float leaderboard_y = ImGui::GetIO().DisplaySize.y; + if (!challenge_icons.empty()) { - // These *should* all be the same square size but you never know. - if (icon->width > max_icon_width) - max_icon_width = icon->width; - sum_of_icon_heights += icon->height; - } - ImGui::SetNextWindowPos( - ImVec2(ImGui::GetIO().DisplaySize.x - 20.f * m_backbuffer_scale - max_icon_width, - ImGui::GetIO().DisplaySize.y - 20.f * m_backbuffer_scale - sum_of_icon_heights)); - ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f)); - if (ImGui::Begin(window_name.c_str(), nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing)) - { - for (const auto& [name, icon] : challenge_icons) + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y), 0, + ImVec2(1.0, 1.0)); + ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f)); + if (ImGui::Begin("Challenges", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing)) { - if (m_challenge_texture_map.find(name) != m_challenge_texture_map.end()) - continue; - const u32 width = icon->width; - const u32 height = icon->height; - TextureConfig tex_config(width, height, 1, 1, 1, AbstractTextureFormat::RGBA8, 0, - AbstractTextureType::Texture_2DArray); - auto res = m_challenge_texture_map.insert_or_assign(name, g_gfx->CreateTexture(tex_config)); - res.first->second->Load(0, width, height, width, icon->rgba_data.data(), - sizeof(u32) * width * height); - } - for (auto& [name, texture] : m_challenge_texture_map) - { - auto icon_itr = challenge_icons.find(name); - if (icon_itr == challenge_icons.end()) + for (const auto& [name, icon] : challenge_icons) { - m_challenge_texture_map.erase(name); - continue; + if (m_challenge_texture_map.find(name) != m_challenge_texture_map.end()) + continue; + const u32 width = icon->width; + const u32 height = icon->height; + TextureConfig tex_config(width, height, 1, 1, 1, AbstractTextureFormat::RGBA8, 0, + AbstractTextureType::Texture_2DArray); + auto res = m_challenge_texture_map.insert_or_assign(name, g_gfx->CreateTexture(tex_config)); + res.first->second->Load(0, width, height, width, icon->rgba_data.data(), + sizeof(u32) * width * height); } - if (texture) + for (auto& [name, texture] : m_challenge_texture_map) { - ImGui::Image(texture.get(), ImVec2(static_cast(icon_itr->second->width), - static_cast(icon_itr->second->height))); + auto icon_itr = challenge_icons.find(name); + if (icon_itr == challenge_icons.end()) + { + m_challenge_texture_map.erase(name); + continue; + } + if (texture) + { + ImGui::Image(texture.get(), ImVec2(static_cast(icon_itr->second->width), + static_cast(icon_itr->second->height))); + } } + leaderboard_y -= ImGui::GetWindowHeight(); } + ImGui::End(); } - ImGui::End(); + if (!leaderboard_progress.empty()) + { + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x, leaderboard_y), 0, + ImVec2(1.0, 1.0)); + ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f)); + if (ImGui::Begin("Leaderboards", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing)) + { + for (const auto& value : leaderboard_progress) + ImGui::Text(value.data()); + } + ImGui::End(); + } } #endif // USE_RETRO_ACHIEVEMENTS @@ -400,7 +407,7 @@ void OnScreenUI::Finalize() DrawDebugText(); OSD::DrawMessages(); #ifdef USE_RETRO_ACHIEVEMENTS - DrawChallenges(); + DrawChallengesAndLeaderboards(); #endif // USE_RETRO_ACHIEVEMENTS ImGui::Render(); } diff --git a/Source/Core/VideoCommon/OnScreenUI.h b/Source/Core/VideoCommon/OnScreenUI.h index 9b7aa0d3f8..1acef96901 100644 --- a/Source/Core/VideoCommon/OnScreenUI.h +++ b/Source/Core/VideoCommon/OnScreenUI.h @@ -62,7 +62,7 @@ public: private: void DrawDebugText(); #ifdef USE_RETRO_ACHIEVEMENTS - void DrawChallenges(); + void DrawChallengesAndLeaderboards(); #endif // USE_RETRO_ACHIEVEMENTS // ImGui resources.