// Copyright 2023 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #ifdef USE_RETRO_ACHIEVEMENTS #include "Core/AchievementManager.h" #include #include #include #include #include "Common/HttpRequest.h" #include "Common/Image.h" #include "Common/Logging/Log.h" #include "Common/WorkQueueThread.h" #include "Core/Config/AchievementSettings.h" #include "Core/Core.h" #include "Core/PowerPC/MMU.h" #include "Core/System.h" #include "DiscIO/Blob.h" #include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/VideoEvents.h" static constexpr bool hardcore_mode_enabled = false; static std::unique_ptr DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge); AchievementManager& AchievementManager::GetInstance() { static AchievementManager s_instance; return s_instance; } void AchievementManager::Init() { if (!m_is_runtime_initialized && Config::Get(Config::RA_ENABLED)) { rc_runtime_init(&m_runtime); m_is_runtime_initialized = true; m_queue.Reset("AchievementManagerQueue", [](const std::function& func) { func(); }); m_image_queue.Reset("AchievementManagerImageQueue", [](const std::function& func) { func(); }); LoginAsync("", [](ResponseType r_type) {}); INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager Initialized"); } } void AchievementManager::SetUpdateCallback(UpdateCallback callback) { m_update_callback = std::move(callback); m_update_callback(); } AchievementManager::ResponseType AchievementManager::Login(const std::string& password) { if (!m_is_runtime_initialized) { ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted login (sync) to RetroAchievements server without " "Achievement Manager initialized."); return AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED; } AchievementManager::ResponseType r_type = VerifyCredentials(password); FetchBadges(); if (m_update_callback) 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(AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED); return; } m_queue.EmplaceItem([this, password, callback] { callback(VerifyCredentials(password)); FetchBadges(); if (m_update_callback) m_update_callback(); }); } bool AchievementManager::IsLoggedIn() const { return !Config::Get(Config::RA_API_TOKEN).empty(); } void AchievementManager::HashGame(const std::string& file_path, const ResponseCallback& callback) { if (!m_is_runtime_initialized) { ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted to load game achievements without Achievement Manager initialized."); callback(AchievementManager::ResponseType::MANAGER_NOT_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(AchievementManager::ResponseType::MALFORMED_OBJECT); } } { std::lock_guard lg{m_lock}; if (m_disabled) { INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving."); callback(AchievementManager::ResponseType::EXPIRED_CONTEXT); return; } m_game_hash = std::move(new_hash); } LoadGameSync(callback); }); } void AchievementManager::HashGame(const DiscIO::Volume* volume, const ResponseCallback& callback) { if (!m_is_runtime_initialized) { ERROR_LOG_FMT(ACHIEVEMENTS, "Attempted to load game achievements without Achievement Manager initialized."); callback(AchievementManager::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 = false; { std::lock_guard lg{m_lock}; if (m_loading_volume.get() != nullptr) { disable = true; } else { m_loading_volume = DiscIO::CreateVolume(volume->GetBlobReader().CopyReader()); } } if (disable) { INFO_LOG_FMT(ACHIEVEMENTS, "Disabling Achievement Manager due to hash spam."); SetDisabled(true); callback(AchievementManager::ResponseType::EXPIRED_CONTEXT); return; } 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(AchievementManager::ResponseType::MALFORMED_OBJECT); return; } } { std::lock_guard lg{m_lock}; if (m_disabled) { INFO_LOG_FMT(ACHIEVEMENTS, "Achievements disabled while hash was resolving."); callback(AchievementManager::ResponseType::EXPIRED_CONTEXT); return; } m_game_hash = std::move(new_hash); m_loading_volume.reset(); } LoadGameSync(callback); }); } void AchievementManager::LoadGameSync(const ResponseCallback& callback) { 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); if (m_update_callback) m_update_callback(); callback(fetch_game_data_response); } bool AchievementManager::IsGameLoaded() const { return m_is_game_loaded; } void AchievementManager::LoadUnlockData(const ResponseCallback& callback) { 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)); if (m_update_callback) m_update_callback(); }); } void AchievementManager::ActivateDeactivateAchievements() { 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++) { u32 points = (m_game_data.achievements[ix].category == RC_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ? 0 : m_game_data.achievements[ix].points; auto iter = m_unlock_map.insert( {m_game_data.achievements[ix].id, UnlockStatus{.game_data_index = ix, .points = points}}); ActivateDeactivateAchievement(iter.first->first, enabled, unofficial, encore); } INFO_LOG_FMT(ACHIEVEMENTS, "Achievements (de)activated."); } void AchievementManager::ActivateDeactivateLeaderboards() { bool leaderboards_enabled = Config::Get(Config::RA_LEADERBOARDS_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 && hardcore_mode_enabled) { rc_runtime_activate_lboard(&m_runtime, leaderboard_id, leaderboard.definition, nullptr, 0); m_queue.EmplaceItem([this, leaderboard_id] { FetchBoardInfo(leaderboard_id); if (m_update_callback) 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() { 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)) { if (m_update_callback) 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); } if (m_update_callback) m_update_callback(); }); } if (!IsGameLoaded()) { if (m_update_callback) m_update_callback(); return; } int badgematch = 0; { std::lock_guard lg{m_lock}; badgematch = m_game_badge.name.compare(m_game_data.image_name); } if (badgematch != 0) { m_image_queue.EmplaceItem([this] { std::string name_to_fetch; { std::lock_guard lg{m_lock}; if (m_game_badge.name.compare(m_game_data.image_name) == 0) return; name_to_fetch.assign(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.compare(m_game_data.image_name) != 0) { 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); } if (m_update_callback) 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) { 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.assign(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.compare(achievement.badge_name) != 0) { 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); } if (m_update_callback) 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.assign(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.compare(achievement.badge_name) != 0) { 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); } if (m_update_callback) m_update_callback(); }); } } if (m_update_callback) m_update_callback(); } void AchievementManager::DoFrame() { if (!m_is_game_loaded) return; if (m_framecount == 0x200) { DisplayWelcomeMessage(); } if (m_framecount <= 0x200) { m_framecount++; } Core::RunAsCPUThread([&] { 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); }); if (!m_system) return; time_t current_time = std::time(nullptr); if (difftime(current_time, m_last_ping_time) > 120) { GenerateRichPresence(); m_queue.EmplaceItem([this] { PingRichPresence(m_rich_presence); }); m_last_ping_time = current_time; if (m_update_callback) 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 m_system->GetMMU() .HostTryReadU16(threadguard, address, PowerPC::RequestedAddressSpace::Physical) .value_or(PowerPC::ReadResult(false, 0u)) .value; case 4: return 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) { { std::lock_guard lg{m_lock}; 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; } } if (m_update_callback) m_update_callback(); } std::recursive_mutex& AchievementManager::GetLock() { return m_lock; } bool AchievementManager::IsHardcoreModeActive() const { std::lock_guard lg{m_lock}; if (!Config::Get(Config::RA_HARDCORE_ENABLED)) return false; if (!Core::IsRunning()) return true; if (!IsGameLoaded()) return false; return (m_runtime.trigger_count + m_runtime.lboard_count > 0); } std::string AchievementManager::GetPlayerDisplayName() const { return IsLoggedIn() ? m_display_name : ""; } u32 AchievementManager::GetPlayerScore() const { return IsLoggedIn() ? m_player_score : 0; } const AchievementManager::BadgeStatus& AchievementManager::GetPlayerBadge() const { return m_player_badge; } std::string AchievementManager::GetGameDisplayName() const { return IsGameLoaded() ? m_game_data.title : ""; } AchievementManager::PointSpread AchievementManager::TallyScore() const { PointSpread spread{}; if (!IsGameLoaded()) return spread; for (const auto& entry : m_unlock_map) { 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; } rc_api_fetch_game_data_response_t* AchievementManager::GetGameData() { return &m_game_data; } const AchievementManager::BadgeStatus& AchievementManager::GetGameBadge() const { return m_game_badge; } const AchievementManager::UnlockStatus& AchievementManager::GetUnlockStatus(AchievementId achievement_id) const { return m_unlock_map.at(achievement_id); } AchievementManager::ResponseType AchievementManager::GetAchievementProgress(AchievementId achievement_id, u32* value, u32* target) { if (!IsGameLoaded()) { ERROR_LOG_FMT( ACHIEVEMENTS, "Attempted to request measured data for achievement ID {} when no game is running.", achievement_id); return ResponseType::INVALID_REQUEST; } 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; } AchievementManager::RichPresence AchievementManager::GetRichPresence() { std::lock_guard lg{m_lock}; RichPresence rich_presence = m_rich_presence; return 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); } if (previously_disabled && !disable) INFO_LOG_FMT(ACHIEVEMENTS, "Achievement Manager has been re-enabled."); }; const AchievementManager::NamedIconMap& AchievementManager::GetChallengeIcons() const { return m_active_challenges; } void AchievementManager::CloseGame() { { std::lock_guard lg{m_lock}; if (m_is_game_loaded) { m_is_game_loaded = false; ActivateDeactivateAchievements(); ActivateDeactivateLeaderboards(); ActivateDeactivateRichPresence(); m_game_id = 0; m_game_badge.name = ""; m_unlock_map.clear(); m_leaderboard_map.clear(); rc_api_destroy_fetch_game_data_response(&m_game_data); std::memset(&m_game_data, 0, sizeof(m_game_data)); m_queue.Cancel(); m_image_queue.Cancel(); m_system = nullptr; } } if (m_update_callback) m_update_callback(); INFO_LOG_FMT(ACHIEVEMENTS, "Game closed."); } void AchievementManager::Logout() { { std::lock_guard lg{m_lock}; CloseGame(); m_player_badge.name = ""; Config::SetBaseOrCurrent(Config::RA_API_TOKEN, ""); } if (m_update_callback) m_update_callback(); INFO_LOG_FMT(ACHIEVEMENTS, "Logged out from server."); } void AchievementManager::Shutdown() { CloseGame(); 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."); } void* AchievementManager::FilereaderOpenByFilepath(const char* path_utf8) { auto state = std::make_unique(); state->volume = DiscIO::CreateVolume(path_utf8); if (!state->volume) return nullptr; return state.release(); } void* AchievementManager::FilereaderOpenByVolume(const char* path_utf8) { auto state = std::make_unique(); { auto& instance = GetInstance(); std::lock_guard lg{instance.GetLock()}; state->volume = std::move(instance.GetLoadingVolume()); } if (!state->volume) return nullptr; return state.release(); } void AchievementManager::FilereaderSeek(void* file_handle, int64_t offset, int origin) { switch (origin) { case SEEK_SET: static_cast(file_handle)->position = offset; break; case SEEK_CUR: static_cast(file_handle)->position += offset; break; case SEEK_END: // Unused break; } } int64_t AchievementManager::FilereaderTell(void* file_handle) { return static_cast(file_handle)->position; } size_t AchievementManager::FilereaderRead(void* file_handle, void* buffer, size_t requested_bytes) { FilereaderState* filereader_state = static_cast(file_handle); bool success = (filereader_state->volume->Read(filereader_state->position, requested_bytes, static_cast(buffer), DiscIO::PARTITION_NONE)); if (success) { filereader_state->position += requested_bytes; return requested_bytes; } else { return 0; } } void AchievementManager::FilereaderClose(void* file_handle) { delete static_cast(file_handle); } AchievementManager::ResponseType AchievementManager::VerifyCredentials(const std::string& password) { rc_api_login_response_t login_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_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); std::memset(&m_game_data, 0, sizeof(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); std::memset(&m_game_data, 0, sizeof(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); std::memset(&m_game_data, 0, sizeof(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]; LeaderboardEntry 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[org_entry.index] = 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]; LeaderboardEntry 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[org_entry.index] = 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[leaderboard_id] = 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); return; } const UnlockStatus& status = it->second; u32 index = status.game_data_index; bool active = (rc_runtime_get_achievement(&m_runtime, id) != nullptr); // 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) { activate = false; } // If encore mode is on, activate/deactivate regardless of current unlock status if (activate && !encore) { // 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) { activate = false; } } if (!active && activate) { rc_runtime_activate_achievement(&m_runtime, id, m_game_data.achievements[index].definition, nullptr, 0); } if (active && !activate) rc_runtime_deactivate_achievement(&m_runtime, id); } void AchievementManager::GenerateRichPresence() { Core::RunAsCPUThread([&] { 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); 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) { INFO_LOG_FMT(ACHIEVEMENTS, "Awarded achievement ID {}.", achievement_id); } else { WARN_LOG_FMT(ACHIEVEMENTS, "Failed to award achievement ID {}.", achievement_id); } return r_type; } 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); } 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 (hardcore_mode_enabled) { 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) { 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++; m_queue.EmplaceItem([this, event_id] { AwardAchievement(event_id); }); 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); PointSpread spread = TallyScore(); if (spread.hard_points == spread.total_points) { 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) { 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; } OSD::AddMessage( fmt::format("{} {}", m_game_data.achievements[game_data_index].title, value.data()), OSD::Duration::VERY_LONG, OSD::Color::GREEN, (Config::Get(Config::RA_BADGES_ENABLED)) ? DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge) : nullptr); } void AchievementManager::HandleAchievementPrimedEvent(const rc_runtime_event_t* runtime_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 primed event with id {}.", runtime_event->id); return; } m_active_challenges[it->second.unlocked_badge.name] = DecodeBadgeToOSDIcon(it->second.unlocked_badge.badge); } void AchievementManager::HandleAchievementUnprimedEvent(const rc_runtime_event_t* runtime_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); } void AchievementManager::HandleLeaderboardStartedEvent(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("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); if (m_update_callback) 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: {} \npost_data: {} \nresponse: {}", api_request.url, api_request.post_data == nullptr ? "NULL" : api_request.post_data, 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: {} \npost_data: {}", api_request.url, api_request.post_data == nullptr ? "NULL" : api_request.post_data); 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; } } static std::unique_ptr DecodeBadgeToOSDIcon(const AchievementManager::Badge& badge) { if (badge.empty()) return nullptr; auto icon = std::make_unique(); if (!Common::LoadPNG(badge, &icon->rgba_data, &icon->width, &icon->height)) { ERROR_LOG_FMT(ACHIEVEMENTS, "Error decoding badge."); return nullptr; } return icon; } #endif // USE_RETRO_ACHIEVEMENTS