From 9a57216fd029910bc20ac94885302d3f61241be0 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 11:03:53 -0400 Subject: [PATCH 1/9] Added AwardAchievement to AchievementManager AwardAchievement performs the API call to notify the site that an achievement has been unlocked. As one of the parameters is the game hash (something I overlooked previously; I thought it was the game ID) this change also moves the game hash into a member field. --- Source/Core/Core/AchievementManager.cpp | 26 ++++++++++++++++++++----- Source/Core/Core/AchievementManager.h | 5 ++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 2b6015f382..f1a6d3a763 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -5,7 +5,6 @@ #include "Core/AchievementManager.h" -#include #include #include "Common/HttpRequest.h" @@ -113,11 +112,10 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, }, .close = [](void* file_handle) { delete reinterpret_cast(file_handle); }}; rc_hash_init_custom_filereader(&volume_reader); - std::array game_hash; - if (!rc_hash_generate_from_file(game_hash.data(), RC_CONSOLE_GAMECUBE, iso_path.c_str())) + if (!rc_hash_generate_from_file(m_game_hash.data(), RC_CONSOLE_GAMECUBE, iso_path.c_str())) return; - m_queue.EmplaceItem([this, callback, game_hash] { - const auto resolve_hash_response = ResolveHash(game_hash); + m_queue.EmplaceItem([this, callback] { + const auto resolve_hash_response = ResolveHash(this->m_game_hash); if (resolve_hash_response != ResponseType::SUCCESS || m_game_id == 0) { callback(resolve_hash_response); @@ -366,6 +364,24 @@ void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool en rc_runtime_deactivate_achievement(&m_runtime, id); } +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); + return r_type; +} + // 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 b5cf465915..96e000344d 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -4,6 +4,7 @@ #pragma once #ifdef USE_RETRO_ACHIEVEMENTS +#include #include #include #include @@ -62,6 +63,7 @@ private: void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); + ResponseType AwardAchievement(AchievementId achievement_id); template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, @@ -69,7 +71,8 @@ private: rc_runtime_t m_runtime{}; bool m_is_runtime_initialized = false; - unsigned int m_game_id = 0; + std::array m_game_hash{}; + u32 m_game_id = 0; rc_api_fetch_game_data_response_t m_game_data{}; bool m_is_game_loaded = false; From 69014e13596c167cfcbbf9ec7e406b39e1de60ea Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 11:09:41 -0400 Subject: [PATCH 2/9] Added SubmitLeaderboard to AchievementManager SubmitLeaderboard submits the player's score/time to a leaderboard on the website via an API call. --- Source/Core/Core/AchievementManager.cpp | 19 +++++++++++++++++++ Source/Core/Core/AchievementManager.h | 1 + 2 files changed, 20 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index f1a6d3a763..8397338b04 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -382,6 +382,25 @@ AchievementManager::ResponseType AchievementManager::AwardAchievement(Achievemen 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); + return r_type; +} + // 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 96e000344d..c68a141c63 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -64,6 +64,7 @@ private: void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); ResponseType AwardAchievement(AchievementId achievement_id); + ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value); template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, From 66b8731bdb48247cecb5f1f38d65990ddb74a435 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 11:27:39 -0400 Subject: [PATCH 3/9] Added PingRichPresence to AchievementManager PingRichPresence makes a "ping" API request to the RetroAchievements website with the provided RichPresence string parameter. While there has been talk about tying ping in with session, in its current state the primary purpose of ping is to send the player's Rich Presence to the website. --- Source/Core/Core/AchievementManager.cpp | 16 ++++++++++++++++ Source/Core/Core/AchievementManager.h | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 8397338b04..bf714c6a17 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -401,6 +401,22 @@ AchievementManager::ResponseType AchievementManager::SubmitLeaderboard(Achieveme 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; +} + // 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 c68a141c63..2412e74587 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -19,6 +19,8 @@ #include "Common/WorkQueueThread.h" using AchievementId = u32; +constexpr size_t RP_SIZE = 256; +using RichPresence = std::array; class AchievementManager { @@ -65,6 +67,8 @@ private: ResponseType AwardAchievement(AchievementId achievement_id); ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value); + ResponseType PingRichPresence(const RichPresence& rich_presence); + template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, From 637ca82fc17b6fe27c4a6530eafe8ac6a68b026d Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 11:30:09 -0400 Subject: [PATCH 4/9] Added HandleAchievementTriggeredEvent to AchievementManager HandleAchievementTriggeredEvent is an asynchronous method that processes an event and places a synchronous AwardAchievement call on the work queue. In the process, it also updates the unlock map and makes the ActivateDeactivateAchievement call to determine and adjust the achievement's current active state. --- Source/Core/Core/AchievementManager.cpp | 12 ++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index bf714c6a17..c974ae2e12 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -417,6 +417,18 @@ AchievementManager::PingRichPresence(const RichPresence& rich_presence) return r_type; } +void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event) +{ + auto it = m_unlock_map.find(runtime_event->id); + if (it == m_unlock_map.end()) + return; + it->second.session_unlock_count++; + m_queue.EmplaceItem([this, runtime_event] { AwardAchievement(runtime_event->id); }); + ActivateDeactivateAchievement(runtime_event->id, Config::Get(Config::RA_ACHIEVEMENTS_ENABLED), + Config::Get(Config::RA_UNOFFICIAL_ENABLED), + Config::Get(Config::RA_ENCORE_ENABLED)); +} + // 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 2412e74587..18be428163 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -69,6 +69,8 @@ private: ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value); ResponseType PingRichPresence(const RichPresence& rich_presence); + void HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event); + template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, const std::function& init_request, From a48fab0abfca2700ff345b0f3f8ef1763c1a329f Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 11:31:43 -0400 Subject: [PATCH 5/9] Added HandleLeaderboardTriggeredEvent to AchievementManager HandleLeaderboardTriggeredEvent processes a leaderboard event and asynchronizes via the work queue a synchronous call to SubmitLeaderboard. --- Source/Core/Core/AchievementManager.cpp | 6 ++++++ Source/Core/Core/AchievementManager.h | 1 + 2 files changed, 7 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index c974ae2e12..dce2f89dae 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -429,6 +429,12 @@ void AchievementManager::HandleAchievementTriggeredEvent(const rc_runtime_event_ Config::Get(Config::RA_ENCORE_ENABLED)); } +void AchievementManager::HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event) +{ + m_queue.EmplaceItem( + [this, runtime_event] { SubmitLeaderboard(runtime_event->id, runtime_event->value); }); +} + // 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 18be428163..dca350bf90 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -70,6 +70,7 @@ private: ResponseType PingRichPresence(const RichPresence& rich_presence); void HandleAchievementTriggeredEvent(const rc_runtime_event_t* runtime_event); + void HandleLeaderboardTriggeredEvent(const rc_runtime_event_t* runtime_event); template ResponseType Request(RcRequest rc_request, RcResponse* rc_response, From ed121a40339a7c9a5723150fa06a3ba9762e78cc Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 12:15:43 -0400 Subject: [PATCH 6/9] Added AchievementEventHandler to AchievementManager AchievementEventHandler simply checks which kind of event is triggered and calls the appropriate function. Its primary purpose is as a function to be pointed to. --- Source/Core/Core/AchievementManager.cpp | 13 +++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ 2 files changed, 15 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index dce2f89dae..536f5fe63f 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -202,6 +202,19 @@ void AchievementManager::ActivateDeactivateRichPresence() nullptr, 0); } +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_LBOARD_TRIGGERED: + HandleLeaderboardTriggeredEvent(runtime_event); + break; + } +} + void AchievementManager::CloseGame() { m_is_game_loaded = false; diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index dca350bf90..293ac00202 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -48,6 +48,8 @@ public: void ActivateDeactivateLeaderboards(); void ActivateDeactivateRichPresence(); + void AchievementEventHandler(const rc_runtime_event_t* runtime_event); + void CloseGame(); void Logout(); void Shutdown(); From 1b880564cbc8da9894af065e9ac1cc9c608352a1 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 12:28:58 -0400 Subject: [PATCH 7/9] Added MemoryPeeker to AchievementManager MemoryPeeker is a function passed by pointer into rcheevos DoFrame functionality that forms the lynchpin of the rcheevos runtime - it provides the interface by which rcheevos accesses memory and determines if the fields provided by achievement, leaderboard, and rich presence definitions are meeting the criteria needed. --- Source/Core/Core/AchievementManager.cpp | 34 ++++++++++++++++++++++++- Source/Core/Core/AchievementManager.h | 7 +++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 536f5fe63f..2d4fce9af8 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -9,8 +9,10 @@ #include "Common/HttpRequest.h" #include "Common/WorkQueueThread.h" -#include "Config/AchievementSettings.h" +#include "Core/Config/AchievementSettings.h" #include "Core/Core.h" +#include "Core/PowerPC/MMU.h" +#include "Core/System.h" #include "DiscIO/Volume.h" static constexpr bool hardcore_mode_enabled = false; @@ -62,6 +64,7 @@ void AchievementManager::LoadGameByFilenameAsync(const std::string& iso_path, callback(AchievementManager::ResponseType::MANAGER_NOT_INITIALIZED); return; } + m_system = &Core::System::GetInstance(); struct FilereaderState { int64_t position = 0; @@ -202,6 +205,34 @@ void AchievementManager::ActivateDeactivateRichPresence() nullptr, 0); } +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) + .value_or(PowerPC::ReadResult(false, 0u)) + .value; + case 2: + return m_system->GetMMU() + .HostTryReadU16(threadguard, address) + .value_or(PowerPC::ReadResult(false, 0u)) + .value; + case 4: + return m_system->GetMMU() + .HostTryReadU32(threadguard, address) + .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) @@ -221,6 +252,7 @@ void AchievementManager::CloseGame() m_game_id = 0; m_queue.Cancel(); m_unlock_map.clear(); + m_system = nullptr; ActivateDeactivateAchievements(); ActivateDeactivateLeaderboards(); ActivateDeactivateRichPresence(); diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 293ac00202..d041259c1c 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -22,6 +22,11 @@ using AchievementId = u32; constexpr size_t RP_SIZE = 256; using RichPresence = std::array; +namespace Core +{ +class System; +} + class AchievementManager { public: @@ -48,6 +53,7 @@ public: void ActivateDeactivateLeaderboards(); void ActivateDeactivateRichPresence(); + u32 MemoryPeeker(u32 address, u32 num_bytes, void* ud); void AchievementEventHandler(const rc_runtime_event_t* runtime_event); void CloseGame(); @@ -80,6 +86,7 @@ private: const std::function& process_response); rc_runtime_t m_runtime{}; + Core::System* m_system{}; bool m_is_runtime_initialized = false; std::array m_game_hash{}; u32 m_game_id = 0; From 22ee729055a94e7d5da1a8b3f7dbaa17d7655b25 Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 12:02:28 -0400 Subject: [PATCH 8/9] Added GenerateRichPresence to AchievementManager GenerateRichPresence calls rc_runtime_get_richpresence in rhceevos on the achievement runtime to get the current Rich Presence string, a description of the player's current in-game state based on its memory as fed into a custom-developed script downloaded via FetchGameData. This gets passed into PingRichPresence, but is separated into its own method so it can be used elsewhere locally. --- Source/Core/Core/AchievementManager.cpp | 14 ++++++++++++++ Source/Core/Core/AchievementManager.h | 1 + 2 files changed, 15 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index 2d4fce9af8..ebb0302a53 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -409,6 +409,20 @@ void AchievementManager::ActivateDeactivateAchievement(AchievementId id, bool en rc_runtime_deactivate_achievement(&m_runtime, id); } +RichPresence AchievementManager::GenerateRichPresence() +{ + RichPresence rp_buffer; + Core::RunAsCPUThread([&] { + rc_runtime_get_richpresence( + &m_runtime, rp_buffer.data(), RP_SIZE, + [](unsigned address, unsigned num_bytes, void* ud) { + return static_cast(ud)->MemoryPeeker(address, num_bytes, ud); + }, + this, nullptr); + }); + return rp_buffer; +} + AchievementManager::ResponseType AchievementManager::AwardAchievement(AchievementId achievement_id) { std::string username = Config::Get(Config::RA_USERNAME); diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index d041259c1c..82e7eb723f 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -72,6 +72,7 @@ private: ResponseType FetchUnlockData(bool hardcore); void ActivateDeactivateAchievement(AchievementId id, bool enabled, bool unofficial, bool encore); + RichPresence GenerateRichPresence(); ResponseType AwardAchievement(AchievementId achievement_id); ResponseType SubmitLeaderboard(AchievementId leaderboard_id, int value); From c9c2e17e5a8e245475d34865da4fd2d1fb0e100a Mon Sep 17 00:00:00 2001 From: LillyJadeKatrin Date: Sat, 15 Apr 2023 12:32:52 -0400 Subject: [PATCH 9/9] Added DoFrame to AchievementManager DoFrame is a function called every frame by the emulator so that rcheevos can be properly updated and processed. It requires a memory peeker and an event handler to be passed in; the memory peeker is called repeatedly each frame to measure what's in memory and compare to achievement definitions, and any events thrown by that comparison are sent to the event handler. Also, DoFrame checks for the current system time to determine when to ping rich presence. Rich Presence on the RetroAchievements website updates every two minutes, so if two minutes have elapsed since the previous ping, another ping is sent. --- Source/Core/Core/AchievementManager.cpp | 23 +++++++++++++++++++++++ Source/Core/Core/AchievementManager.h | 2 ++ 2 files changed, 25 insertions(+) diff --git a/Source/Core/Core/AchievementManager.cpp b/Source/Core/Core/AchievementManager.cpp index ebb0302a53..9c8b3d1a4a 100644 --- a/Source/Core/Core/AchievementManager.cpp +++ b/Source/Core/Core/AchievementManager.cpp @@ -14,6 +14,7 @@ #include "Core/PowerPC/MMU.h" #include "Core/System.h" #include "DiscIO/Volume.h" +#include "VideoCommon/VideoEvents.h" static constexpr bool hardcore_mode_enabled = false; @@ -205,6 +206,28 @@ void AchievementManager::ActivateDeactivateRichPresence() nullptr, 0); } +void AchievementManager::DoFrame() +{ + if (!m_is_game_loaded) + return; + Core::RunAsCPUThread([&] { + rc_runtime_do_frame( + &m_runtime, + [](const rc_runtime_event_t* runtime_event) { + AchievementManager::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; + u64 current_time = m_system->GetCoreTiming().GetTicks(); + if (current_time - m_last_ping_time > SystemTimers::GetTicksPerSecond() * 120) + m_queue.EmplaceItem([this] { PingRichPresence(GenerateRichPresence()); }); +} + u32 AchievementManager::MemoryPeeker(u32 address, u32 num_bytes, void* ud) { if (!m_system) diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h index 82e7eb723f..b6cbcde732 100644 --- a/Source/Core/Core/AchievementManager.h +++ b/Source/Core/Core/AchievementManager.h @@ -53,6 +53,7 @@ public: void ActivateDeactivateLeaderboards(); void ActivateDeactivateRichPresence(); + void DoFrame(); u32 MemoryPeeker(u32 address, u32 num_bytes, void* ud); void AchievementEventHandler(const rc_runtime_event_t* runtime_event); @@ -93,6 +94,7 @@ private: u32 m_game_id = 0; rc_api_fetch_game_data_response_t m_game_data{}; bool m_is_game_loaded = false; + u64 m_last_ping_time = 0; struct UnlockStatus {