From 505f40cf9d6df3a2f5985132505caacf4125d403 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Wed, 12 Apr 2023 21:46:27 -0400 Subject: [PATCH 1/5] Added ActivateDeactivateAchievement to AchievementManager ActivateDeactivateAchievement is passed an Achievement ID as returned from the FetchGameData API call and determines whether to activate it, deactivate it, or leave it where it is based on its current known state and what settings are enabled. Activating or deactivating an achievement entails calling a method provided by rcheevos that performs this on the rcheevos runtime. Activating an achievement loads its memory signature into the runtime; now the runtime will process the achievement each time the rc_runtime_do_frame function is called (this will be in a future PR) to determine when the achievement's requirements are met. Deactivating an achievement unloads it from the runtime. The specific logic to determine whether an achievement is active operates over many fields but is documented in detail inside the function. There are multiple settings flags for which achievements are enabled (one flag for all achievements, an "unofficial" flag for enabling achievements marked as unofficial i.e. those that have logic on the site but have not yet been officially approved, and an "encore" flag that enables achievements the player has already unlocked) and this function also evaluates whether the achievement has been unlocked in hardcore mode or softcore mode (though currently every reference to the current hardcore mode state is hardcoded as false). --- Source/Core/Core/AchievementManager.cpp | 49 +++++++++++++++++++ Source/Core/Core/AchievementManager.h | 18 +++++++ .../Core/Core/Config/AchievementSettings.cpp | 5 ++ Source/Core/Core/Config/AchievementSettings.h | 3 ++ 4 files changed, 75 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index a566ae6a63..84329b1f10 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -14,6 +14,8 @@ #include "Core/Core.h" #include "DiscIO/Volume.h" +static constexpr bool hardcore_mode_enabled = false; + AchievementManager* AchievementManager::GetInstance() { static AchievementManager s_instance; @@ -138,6 +140,7 @@ void AchievementManager::CloseGame() m_is_game_loaded = false; m_game_id = 0; m_queue.Cancel(); + m_unlock_map.clear(); } void AchievementManager::Logout() @@ -212,6 +215,52 @@ AchievementManager::ResponseType AchievementManager::FetchGameData() rc_api_process_fetch_game_data_response); } +void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool enabled, + bool unofficial, bool encore) +{ + auto it = m_unlock_map.find(id); + if (it == m_unlock_map.end()) + 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); +} + // Every RetroAchievements API call, with only a partial exception for fetch_image, follows // the same design pattern (here, X is the name of the call): // Create a specific rc_api_X_request_t struct and populate with the necessary values diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index ecbf236d60..511a573d23 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,8 @@ #include "Common/Event.h" #include "Common/WorkQueueThread.h" +using AchievementId = u32; + class AchievementManager { public: @@ -50,6 +53,8 @@ private: ResponseType StartRASession(); ResponseType FetchGameData(); + void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); + template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, @@ -61,6 +66,19 @@ private: rc_api_fetch_game_data_response_t m_game_data{}; bool m_is_game_loaded = false; + struct UnlockStatus + { + AchievementId game_data_index = 0; + enum class UnlockType + { + LOCKED, + SOFTCORE, + HARDCORE + } remote_unlock_status = UnlockType::LOCKED; + int session_unlock_count = 0; + }; + std::unordered_map m_unlock_map; + Common::WorkQueueThread> m_queue; }; // class AchievementManager diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp index f7ec642881..92a761f6a6 100644 --- a/Source/Core/Core/Config/AchievementSettings.cpp +++ b/Source/Core/Core/Config/AchievementSettings.cpp @@ -13,4 +13,9 @@ namespace Config const Info RA_ENABLED{{System::Achievements, "Achievements", "Enabled"}, false}; 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_UNOFFICIAL_ENABLED{{System::Achievements, "Achievements", "UnofficialEnabled"}, + false}; +const Info RA_ENCORE_ENABLED{{System::Achievements, "Achievements", "EncoreEnabled"}, false}; } // namespace Config diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h index 8f26769d7f..13da23eafa 100644 --- a/Source/Core/Core/Config/AchievementSettings.h +++ b/Source/Core/Core/Config/AchievementSettings.h @@ -11,4 +11,7 @@ namespace Config extern const Info RA_ENABLED; extern const Info RA_USERNAME; extern const Info RA_API_TOKEN; +extern const Info RA_ACHIEVEMENTS_ENABLED; +extern const Info RA_UNOFFICIAL_ENABLED; +extern const Info RA_ENCORE_ENABLED; } // namespace Config From db44e10057893deb614fc67cd8c87515bae7d53f Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Wed, 12 Apr 2023 21:43:07 -0400 Subject: [PATCH 2/5] Added FetchUnlockData to AchievementManager FetchUnlockData is an API call to RetroAchievements that downloads a list of achievement IDs for a game that the user has already unlocked and published to the site. It accepts a parameter for whether or not hardcore or softcore achievements are being requested, so that must be provided as well. Once it has the requested list on hand, it updates each achievement's status in the unlock map and will activate or deactivate achievements as necessary. --- Source/Core/Core/AchievementManager.cpp | 33 +++++++++++++++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ 2 files changed, 35 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 84329b1f10..e6b5df8bd9 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -215,6 +215,39 @@ AchievementManager::ResponseType AchievementManager::FetchGameData() rc_api_process_fetch_game_data_response); } +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; +} + void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore) { diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 511a573d23..0e0c2668d9 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -52,6 +52,7 @@ private: ResponseType ResolveHash(std::array game_hash); ResponseType StartRASession(); ResponseType FetchGameData(); + ResponseType FetchUnlockData(bool hardcore); void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); @@ -80,6 +81,7 @@ private: std::unordered_map m_unlock_map; Common::WorkQueueThread> m_queue; + std::recursive_mutex m_lock; }; // class AchievementManager #endif // USE_RETRO_ACHIEVEMENTS From da1de36cb9b46a13cff4cd610516d19e3799923f Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Wed, 12 Apr 2023 08:51:51 -0400 Subject: [PATCH 3/5] Added LoadUnlockData and ActivateDeactivateAchievements to AchievementManager LoadUnlockData and ActivateDeactivateAchievements are the public API components responding to the FetchUnlocks and A/DAchievement (singular) private methods. LoadUnlockData is asynchronous and performs both a hardcore and a softcore unlock call, updating the unlock map and the active status of any achievements returned from these calls. ActivateDeactivateAchievements calls ActivateDeactivateAchievement on every achievement ID found in m_game_data, initializing the unlock map for each ID if not already found. Both of these are currently called in LoadGameByFilenameAsync once the game has been loaded properly. There's a lock around this, to ensure that the unlock map is initialized properly by ActivateDeactivate Achievements before FetchUnlockData makes modifications to it without stalling the async portions of FetchUnlockData. --- Source/Core/Core/AchievementManager.cpp | 39 +++++++++++++++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ 2 files changed, 41 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index e6b5df8bd9..11061073fe 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -131,16 +131,55 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, const auto fetch_game_data_response = FetchGameData(); m_is_game_loaded = fetch_game_data_response == ResponseType::SUCCESS; + + // 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}; + LoadUnlockData([](ResponseType r_type) {}); + ActivateDeactivateAchievements(); + } + callback(fetch_game_data_response); }); } +void AchievementManager::LoadUnlockData(const ResponseCallback& callback) +{ + m_queue.EmplaceItem([this, callback] { + const auto hardcore_unlock_response = FetchUnlockData(true); + if (hardcore_unlock_response != ResponseType::SUCCESS) + { + callback(hardcore_unlock_response); + return; + } + + callback(FetchUnlockData(false)); + }); +} + +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++) + { + auto iter = + m_unlock_map.insert({m_game_data.achievements[ix].id, UnlockStatus{.game_data_index = ix}}); + ActivateDeactivateAchievement(iter.first->first, enabled, unofficial, encore); + } +} + void AchievementManager::CloseGame() { m_is_game_loaded = false; m_game_id = 0; m_queue.Cancel(); m_unlock_map.clear(); + ActivateDeactivateAchievements(); } void AchievementManager::Logout() diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 0e0c2668d9..7eb6397e49 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -39,6 +39,8 @@ public: bool IsLoggedIn() const; void LoadGameByFilenameAsync(const std::string& iso_path, const ResponseCallback& callback); + void LoadUnlockData(const ResponseCallback& callback); + void ActivateDeactivateAchievements(); void CloseGame(); void Logout(); void Shutdown(); From 64e3a64c87d6a7880be47d14bf97174f791a2d58 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Thu, 13 Apr 2023 23:34:46 -0400 Subject: [PATCH 4/5] Added ActivateDeactivateLeaderboards to AchievementManager This activates or deactivates leaderboards in the rcheevos runtime similarly to achievements. The logic is much more straightforward - all leaderboards are active together; there is nothing requiring some leaderboards to be active while others are unactive, and even a leaderboard that has been submitted to in this session is still active to be submitted to again. The only criteria are that leaderboards must be enabled in the settings, and hardcore mode must be on, the latter of which is false until a future PR. --- Source/Core/Core/AchievementManager.cpp | 19 +++++++++++++++++++ Source/Core/Core/AchievementManager.h | 1 + .../Core/Core/Config/AchievementSettings.cpp | 2 ++ Source/Core/Core/Config/AchievementSettings.h | 1 + 4 files changed, 23 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 11061073fe..1b44ab8a4e 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -141,6 +141,7 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, LoadUnlockData([](ResponseType r_type) {}); ActivateDeactivateAchievements(); } + ActivateDeactivateLeaderboards(); callback(fetch_game_data_response); }); @@ -173,6 +174,23 @@ void AchievementManager::ActivateDeactivateAchievements() } } +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]; + if (m_is_game_loaded && leaderboards_enabled && hardcore_mode_enabled) + { + rc_runtime_activate_lboard(&m_runtime, leaderboard.id, leaderboard.definition, nullptr, 0); + } + else + { + rc_runtime_deactivate_lboard(&m_runtime, m_game_data.leaderboards[ix].id); + } + } +} + void AchievementManager::CloseGame() { m_is_game_loaded = false; @@ -180,6 +198,7 @@ void AchievementManager::CloseGame() m_queue.Cancel(); m_unlock_map.clear(); ActivateDeactivateAchievements(); + ActivateDeactivateLeaderboards(); } void AchievementManager::Logout() diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 7eb6397e49..d2e37329f0 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -41,6 +41,7 @@ public: void LoadUnlockData(const ResponseCallback& callback); void ActivateDeactivateAchievements(); + void ActivateDeactivateLeaderboards(); void CloseGame(); void Logout(); void Shutdown(); diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp index 92a761f6a6..6da342cbc4 100644 --- a/Source/Core/Core/Config/AchievementSettings.cpp +++ b/Source/Core/Core/Config/AchievementSettings.cpp @@ -15,6 +15,8 @@ const Info RA_USERNAME{{System::Achievements, "Achievements", "User 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_UNOFFICIAL_ENABLED{{System::Achievements, "Achievements", "UnofficialEnabled"}, false}; const Info RA_ENCORE_ENABLED{{System::Achievements, "Achievements", "EncoreEnabled"}, false}; diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h index 13da23eafa..f49537ec39 100644 --- a/Source/Core/Core/Config/AchievementSettings.h +++ b/Source/Core/Core/Config/AchievementSettings.h @@ -12,6 +12,7 @@ extern const Info RA_ENABLED; extern const Info RA_USERNAME; extern const Info RA_API_TOKEN; extern const Info RA_ACHIEVEMENTS_ENABLED; +extern const Info RA_LEADERBOARDS_ENABLED; extern const Info RA_UNOFFICIAL_ENABLED; extern const Info RA_ENCORE_ENABLED; } // namespace Config From 31c3288fd52704f41dde9c7304b0233c53accb19 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Thu, 13 Apr 2023 23:35:49 -0400 Subject: [PATCH 5/5] Added ActivateDeactivateRichPresence to AchievementManager RetroAchievements Rich Presence is a script that is run periodically on a game's memory to provide a detailed text description of what the player is doing. Existing Discord presence on Dolphin would update a player's Discord status to say not just that they are using Dolphin but that they are playing, for example, Sonic Adventure 2 Battle; Rich Presence would detail that the player is in City Escape with 5 lives and 142 rings. Activating this in the runtime simply entails loading that text script, as returned by the FetchGameData API call, into the runtime, here only determined by whether rich presence is enabled in the achievement settings. Deactivating this is done via the same rcheevos method by setting the rich presence to an empty string. --- Source/Core/Core/AchievementManager.cpp | 12 ++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ Source/Core/Core/Config/AchievementSettings.cpp | 2 ++ Source/Core/Core/Config/AchievementSettings.h | 1 + 4 files changed, 17 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 1b44ab8a4e..a274ac11b7 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -142,6 +142,7 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, ActivateDeactivateAchievements(); } ActivateDeactivateLeaderboards(); + ActivateDeactivateRichPresence(); callback(fetch_game_data_response); }); @@ -191,6 +192,16 @@ void AchievementManager::ActivateDeactivateLeaderboards() } } +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); +} + void AchievementManager::CloseGame() { m_is_game_loaded = false; @@ -199,6 +210,7 @@ void AchievementManager::CloseGame() m_unlock_map.clear(); ActivateDeactivateAchievements(); ActivateDeactivateLeaderboards(); + ActivateDeactivateRichPresence(); } void AchievementManager::Logout() diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index d2e37329f0..b759386721 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -42,6 +42,8 @@ public: void LoadUnlockData(const ResponseCallback& callback); void ActivateDeactivateAchievements(); void ActivateDeactivateLeaderboards(); + void ActivateDeactivateRichPresence(); + void CloseGame(); void Logout(); void Shutdown(); diff --git a/Source/Core/Core/Config/AchievementSettings.cpp b/Source/Core/Core/Config/AchievementSettings.cpp index 6da342cbc4..3499fcc791 100644 --- a/Source/Core/Core/Config/AchievementSettings.cpp +++ b/Source/Core/Core/Config/AchievementSettings.cpp @@ -17,6 +17,8 @@ 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_UNOFFICIAL_ENABLED{{System::Achievements, "Achievements", "UnofficialEnabled"}, false}; const Info RA_ENCORE_ENABLED{{System::Achievements, "Achievements", "EncoreEnabled"}, false}; diff --git a/Source/Core/Core/Config/AchievementSettings.h b/Source/Core/Core/Config/AchievementSettings.h index f49537ec39..33318f7400 100644 --- a/Source/Core/Core/Config/AchievementSettings.h +++ b/Source/Core/Core/Config/AchievementSettings.h @@ -13,6 +13,7 @@ extern const Info RA_USERNAME; 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_UNOFFICIAL_ENABLED; extern const Info RA_ENCORE_ENABLED; } // namespace Config