From f372a92246770eaec0dc0753b75aed2c6a521dab Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 15 Jan 2026 23:12:51 +1000 Subject: [PATCH] Achievements: Add active leaderboard trackers to pause menu --- src/core/achievements.cpp | 48 +++-- src/core/achievements_private.h | 1 + src/core/fullscreenui.cpp | 2 +- src/core/fullscreenui_achievements.cpp | 237 +++++++++++++++++++++---- src/core/fullscreenui_private.h | 2 +- 5 files changed, 233 insertions(+), 57 deletions(-) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 9816c052a..05517d810 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -1440,25 +1440,36 @@ void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event) } } +std::string_view Achievements::GetLeaderboardFormatIcon(u32 format) +{ + static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + ICON_EMOJI_CLOCK_FIVE_OCLOCK, + ICON_EMOJI_DIRECT_HIT, + ICON_EMOJI_CLIPBOARD, + }; + + return value_strings[std::min(format, std::size(value_strings) - 1)]; +} + void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* event) { DEV_LOG("Leaderboard {} ({}) submitted", event->leaderboard->id, event->leaderboard->title); if (g_settings.achievements_leaderboard_notifications) { - static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS][2] = { - {ICON_EMOJI_CLOCK_FIVE_OCLOCK, TRANSLATE_NOOP("Achievements", "Your Time: {}")}, - {ICON_EMOJI_DIRECT_HIT, TRANSLATE_NOOP("Achievements", "Your Score: {}")}, - {ICON_EMOJI_CLIPBOARD, TRANSLATE_NOOP("Achievements", "Your Value: {}")}, + static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + TRANSLATE_NOOP("Achievements", "Your Time: {}"), + TRANSLATE_NOOP("Achievements", "Your Score: {}"), + TRANSLATE_NOOP("Achievements", "Your Value: {}"), }; - std::string message = fmt::format( - "{} {}", value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)][0], - TinyString::from_format( - fmt::runtime(Host::TranslateToStringView( - "Achievements", - value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)][1])), - event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown")); + std::string message = + fmt::format("{} {}", GetLeaderboardFormatIcon(event->leaderboard->format), + TinyString::from_format( + fmt::runtime(Host::TranslateToStringView( + "Achievements", + value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), + event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown")); FullscreenUI::AddAchievementNotification( fmt::format("leaderboard_{}", event->leaderboard->id), @@ -1479,21 +1490,20 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve if (g_settings.achievements_leaderboard_notifications) { - static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS][2] = { - {ICON_EMOJI_CLOCK_FIVE_OCLOCK, TRANSLATE_NOOP("Achievements", "Your Time: {0} (Best: {1})")}, - {ICON_EMOJI_DIRECT_HIT, TRANSLATE_NOOP("Achievements", "Your Score: {0} (Best: {1})")}, - {ICON_EMOJI_CLIPBOARD, TRANSLATE_NOOP("Achievements", "Your Value: {0} (Best: {1})")}, + static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = { + TRANSLATE_NOOP("Achievements", "Your Time: {0} (Best: {1})"), + TRANSLATE_NOOP("Achievements", "Your Score: {0} (Best: {1})"), + TRANSLATE_NOOP("Achievements", "Your Value: {0} (Best: {1})"), }; std::string message = fmt::format( - "{} {}\n" ICON_EMOJI_BAR_CHART " {}", - value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)][0], + "{} {}\n" ICON_EMOJI_BAR_CHART " {}", GetLeaderboardFormatIcon(event->leaderboard->format), TinyString::from_format( fmt::runtime(Host::TranslateToStringView( "Achievements", - value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)][1])), + value_strings[std::min(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])), event->leaderboard_scoreboard->submitted_score, event->leaderboard_scoreboard->best_score), - TinyString::from_format(TRANSLATE_FS("Achievements", "Your Position: {0} of {1}"), + TinyString::from_format(TRANSLATE_FS("Achievements", "Leaderboard Position: {0} of {1}"), event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries)); FullscreenUI::AddAchievementNotification(fmt::format("leaderboard_{}", event->leaderboard->id), diff --git a/src/core/achievements_private.h b/src/core/achievements_private.h index 4790325f7..55760cf38 100644 --- a/src/core/achievements_private.h +++ b/src/core/achievements_private.h @@ -55,6 +55,7 @@ std::optional& GetActiveProgressIndicator(); std::string GetAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked, bool download_if_missing = true); std::string GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry); +std::string_view GetLeaderboardFormatIcon(u32 format); std::string GetSubsetBadgePath(const rc_client_subset_t* subset); diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index bc14f19e5..bc3f34310 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -312,7 +312,7 @@ void FullscreenUI::OpenPauseMenu() ForceKeyNavEnabled(); EnqueueSoundEffect(SFX_NAV_ACTIVATE); - UpdateAchievementsRecentUnlockAndAlmostThere(); + UpdateAchievementsPauseScreenInfo(); BeginTransition(SHORT_TRANSITION_TIME, []() { s_locals.current_pause_submenu = PauseSubMenu::None; SwitchToMainWindow(MainWindowType::PauseMenu); diff --git a/src/core/fullscreenui_achievements.cpp b/src/core/fullscreenui_achievements.cpp index 93dd66008..34b8ca7fe 100644 --- a/src/core/fullscreenui_achievements.cpp +++ b/src/core/fullscreenui_achievements.cpp @@ -87,6 +87,15 @@ struct PauseMenuTimedMeasuredAchievementInfo : PauseMenuMeasuredAchievementInfo Timer::Value show_time; }; +struct PauseMenuLeaderboardInfo +{ + std::string title; + std::string description; + std::string tracker_value; + u32 leaderboard_id; + u32 format; +}; + } // namespace static void DrawNotifications(NotificationLayout& layout); @@ -150,6 +159,7 @@ struct AchievementsLocals std::optional most_recent_unlock; std::optional achievement_nearest_completion; std::optional most_recent_progress_update; + std::vector active_leaderboards; rc_client_leaderboard_list_t* leaderboard_list = nullptr; const rc_client_leaderboard_t* open_leaderboard = nullptr; @@ -715,59 +725,111 @@ void FullscreenUI::CachePauseMenuAchievementInfo(const rc_client_achievement_t* value->show_time = Timer::GetCurrentValue(); } -void FullscreenUI::UpdateAchievementsRecentUnlockAndAlmostThere() +void FullscreenUI::UpdateAchievementsPauseScreenInfo() { const auto lock = Achievements::GetLock(); if (!Achievements::HasActiveGame()) { s_achievements_locals.most_recent_unlock.reset(); s_achievements_locals.achievement_nearest_completion.reset(); + s_achievements_locals.active_leaderboards.clear(); return; } rc_client_achievement_list_t* const achievements = - rc_client_create_achievement_list(Achievements::GetClient(), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, - RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - if (!achievements) + Achievements::HasAchievements() ? + rc_client_create_achievement_list(Achievements::GetClient(), RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) : + nullptr; + if (achievements) { - s_achievements_locals.most_recent_unlock.reset(); - s_achievements_locals.achievement_nearest_completion.reset(); - return; - } + const rc_client_achievement_t* most_recent_unlock = nullptr; + const rc_client_achievement_t* nearest_completion = nullptr; - const rc_client_achievement_t* most_recent_unlock = nullptr; - const rc_client_achievement_t* nearest_completion = nullptr; - - for (u32 i = 0; i < achievements->num_buckets; i++) - { - const rc_client_achievement_bucket_t& bucket = achievements->buckets[i]; - for (u32 j = 0; j < bucket.num_achievements; j++) + for (u32 i = 0; i < achievements->num_buckets; i++) { - const rc_client_achievement_t* achievement = bucket.achievements[j]; + const rc_client_achievement_bucket_t& bucket = achievements->buckets[i]; + for (u32 j = 0; j < bucket.num_achievements; j++) + { + const rc_client_achievement_t* achievement = bucket.achievements[j]; - if (achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) - { - if (!most_recent_unlock || achievement->unlock_time > most_recent_unlock->unlock_time) - most_recent_unlock = achievement; - } - else - { - // find the achievement with the greatest normalized progress, but skip anything below 80%, - // matching the rc_client definition of "almost there" - const float percent_cutoff = 80.0f; - if (achievement->measured_percent >= percent_cutoff && - (!nearest_completion || achievement->measured_percent > nearest_completion->measured_percent)) + if (achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { - nearest_completion = achievement; + if (!most_recent_unlock || achievement->unlock_time > most_recent_unlock->unlock_time) + most_recent_unlock = achievement; + } + else + { + // find the achievement with the greatest normalized progress, but skip anything below 80%, + // matching the rc_client definition of "almost there" + const float percent_cutoff = 80.0f; + if (achievement->measured_percent >= percent_cutoff && + (!nearest_completion || achievement->measured_percent > nearest_completion->measured_percent)) + { + nearest_completion = achievement; + } } } } + + CachePauseMenuAchievementInfo(most_recent_unlock, s_achievements_locals.most_recent_unlock); + CachePauseMenuAchievementInfo(nearest_completion, s_achievements_locals.achievement_nearest_completion); + + rc_client_destroy_achievement_list(achievements); + } + else + { + s_achievements_locals.most_recent_unlock.reset(); + s_achievements_locals.achievement_nearest_completion.reset(); } - CachePauseMenuAchievementInfo(most_recent_unlock, s_achievements_locals.most_recent_unlock); - CachePauseMenuAchievementInfo(nearest_completion, s_achievements_locals.achievement_nearest_completion); + rc_client_leaderboard_list_t* const leaderboards = + Achievements::HasLeaderboards() ? + rc_client_create_leaderboard_list(Achievements::GetClient(), RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) : + nullptr; + if (leaderboards) + { + std::vector& active_lbs = s_achievements_locals.active_leaderboards; + size_t num_active_lbs = 0; - rc_client_destroy_achievement_list(achievements); + for (u32 i = 0; i < leaderboards->num_buckets; i++) + { + const rc_client_leaderboard_bucket_t& bucket = leaderboards->buckets[i]; + for (u32 j = 0; j < bucket.num_leaderboards; j++) + { + const rc_client_leaderboard_t* leaderboard = bucket.leaderboards[j]; + if (leaderboard->state != RC_CLIENT_LEADERBOARD_STATE_TRACKING) + continue; + + // avoid alloc if unnecessary + if (num_active_lbs >= active_lbs.size() || active_lbs[num_active_lbs].leaderboard_id != leaderboard->id) + { + if (num_active_lbs < active_lbs.size()) + active_lbs.erase(active_lbs.begin() + num_active_lbs, active_lbs.end()); + + PauseMenuLeaderboardInfo& lbinfo = active_lbs.emplace_back(); + lbinfo.title = leaderboard->title; + if (leaderboard->description) + lbinfo.description = leaderboard->description; + } + + if (leaderboard->tracker_value) + active_lbs[num_active_lbs].tracker_value = leaderboard->tracker_value; + + num_active_lbs++; + } + } + + // remove extras + if (num_active_lbs < active_lbs.size()) + active_lbs.erase(active_lbs.begin() + num_active_lbs, active_lbs.end()); + + rc_client_destroy_leaderboard_list(leaderboards); + } + else + { + s_achievements_locals.active_leaderboards.clear(); + } } void FullscreenUI::UpdateAchievementsLastProgressUpdate(const rc_client_achievement_t* achievement) @@ -971,10 +1033,11 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) const auto get_achievement_height = [&badge_size, &badge_text_width, &text_spacing](std::string_view description) { const ImVec2 description_size = - description.empty() ? ImVec2(0.0f, 0.0f) : + description.empty() ? ImVec2() : UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, badge_text_width, IMSTR_START_END(description)); - const float text_height = UIStyle.MediumSmallFontSize + text_spacing + description_size.y; + const float text_height = + UIStyle.MediumSmallFontSize + (description.empty() ? 0.0f : (text_spacing + description_size.y)); return std::max(text_height, badge_size); }; @@ -986,7 +1049,7 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) ImVec2 badge_text_pos = ImVec2(image_max.x + (text_spacing * 3.0f), text_pos.y); const ImVec4 clip_rect = ImVec4(badge_text_pos.x, badge_text_pos.y, badge_text_pos.x + badge_text_width, box_max.y); ImVec2 text_size = description.empty() ? - ImVec2(0.0f, 0.0f) : + ImVec2() : UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, badge_text_width, IMSTR_START_END(description)); @@ -997,11 +1060,12 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) { dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, badge_text_pos, title_text_color, IMSTR_START_END(title), 0.0f, &clip_rect); - badge_text_pos.y += UIStyle.MediumSmallFontSize + text_spacing; + badge_text_pos.y += UIStyle.MediumSmallFontSize; } if (!description.empty()) { + badge_text_pos.y += text_spacing; dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, badge_text_pos, text_color, IMSTR_START_END(description), badge_text_width, &clip_rect); badge_text_pos.y += text_size.y; @@ -1091,6 +1155,107 @@ void FullscreenUI::DrawAchievementsPauseMenuOverlays(float start_pos_y) text_pos.y += paragraph_spacing; } } + + // Leaderboards + if (!s_achievements_locals.active_leaderboards.empty()) + { + box_height = box_padding + box_padding + UIStyle.MediumFontSize; + + const std::string_view icon_template = Achievements::GetLeaderboardFormatIcon(RC_CLIENT_LEADERBOARD_FORMAT_TIME); + const float leaderboard_icon_size = + UIStyle.Font + ->CalcTextSizeA(UIStyle.LargeFontSize, UIStyle.NormalFontWeight, FLT_MAX, 0.0f, IMSTR_START_END(icon_template)) + .x; + const float leaderboard_icon_reserve = leaderboard_icon_size + (text_spacing * 3.0f); + const float avail_text_width = box_content_width - leaderboard_icon_reserve; + + for (size_t i = 0; i < s_achievements_locals.active_leaderboards.size(); i++) + { + const PauseMenuLeaderboardInfo& lbinfo = s_achievements_locals.active_leaderboards[i]; + box_height += paragraph_spacing; + + const ImVec2 tracker_size = lbinfo.tracker_value.empty() ? + ImVec2() : + UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + FLT_MAX, 0.0f, IMSTR_START_END(lbinfo.tracker_value)); + const float avail_title_width = + avail_text_width - ((tracker_size.x > 0.0f) ? (tracker_size.x + text_spacing) : 0.0f); + const ImVec2 title_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + FLT_MAX, avail_title_width, IMSTR_START_END(lbinfo.title)); + box_height += title_size.y; + + if (!lbinfo.description.empty()) + { + const ImVec2 description_size = + UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, avail_text_width, + IMSTR_START_END(lbinfo.description)); + box_height += text_spacing + description_size.y; + } + + box_height += ((i == (s_achievements_locals.active_leaderboards.size() - 1)) ? 0.0f : paragraph_spacing); + } + + box_min = ImVec2(box_min.x, box_max.y + box_margin); + box_max = ImVec2(box_min.x + box_width, box_min.y + box_height); + text_pos = ImVec2(box_min.x + box_padding, box_min.y + box_padding); + + dl->AddRectFilled(box_min, box_max, box_background_color, box_rounding); + + buffer.format(ICON_FA_STOPWATCH " {}", + TRANSLATE_DISAMBIG_SV("Achievements", "Active Leaderboard Attempts", "Pause Menu")); + dl->AddText(UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, text_pos, box_title_text_color, + IMSTR_START_END(buffer)); + text_pos.y += UIStyle.MediumFontSize; + + const ImVec4 clip_rect = ImVec4(text_pos.x, text_pos.y, text_pos.x + box_content_width, text_pos.y + box_height); + + for (const PauseMenuLeaderboardInfo& lbinfo : s_achievements_locals.active_leaderboards) + { + text_pos.y += paragraph_spacing; + + const std::string_view icon = Achievements::GetLeaderboardFormatIcon(lbinfo.format); + dl->AddText(UIStyle.Font, UIStyle.LargeFontSize, UIStyle.NormalFontWeight, text_pos, title_text_color, + IMSTR_START_END(icon), 0.0f, &clip_rect); + + const ImVec2 tracker_size = lbinfo.tracker_value.empty() ? + ImVec2() : + UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + FLT_MAX, 0.0f, IMSTR_START_END(lbinfo.tracker_value)); + const float avail_title_width = + avail_text_width - ((tracker_size.x > 0.0f) ? (tracker_size.x + text_spacing) : 0.0f); + const ImVec2 title_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + FLT_MAX, avail_title_width, IMSTR_START_END(lbinfo.title)); + + dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + ImVec2(text_pos.x + leaderboard_icon_reserve, text_pos.y), title_text_color, + IMSTR_START_END(lbinfo.title), avail_title_width, &clip_rect); + + if (!lbinfo.tracker_value.empty()) + { + dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.BoldFontWeight, + ImVec2(box_max.x - box_padding - tracker_size.x, text_pos.y), title_text_color, + IMSTR_START_END(lbinfo.tracker_value), 0.0f, &clip_rect); + } + + text_pos.y += title_size.y; + + if (!lbinfo.description.empty()) + { + text_pos.y += text_spacing; + + const ImVec2 description_size = + UIStyle.Font->CalcTextSizeA(UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, FLT_MAX, avail_text_width, + IMSTR_START_END(lbinfo.description)); + dl->AddText(UIStyle.Font, UIStyle.MediumSmallFontSize, UIStyle.NormalFontWeight, + ImVec2(text_pos.x + leaderboard_icon_reserve, text_pos.y), text_color, + IMSTR_START_END(lbinfo.description), avail_text_width, &clip_rect); + + text_pos.y += description_size.y; + } + + text_pos.y += paragraph_spacing; + } + } } void FullscreenUI::OpenAchievementsWindow() diff --git a/src/core/fullscreenui_private.h b/src/core/fullscreenui_private.h index ff1bf0393..c0953ca51 100644 --- a/src/core/fullscreenui_private.h +++ b/src/core/fullscreenui_private.h @@ -131,7 +131,7 @@ void DrawAchievementsPauseMenuOverlays(float start_pos_y); /// Updates the stored most-recent and closest-to-completion achievements. /// Call before calling DrawPauseMenuOverlays() for the first time. -void UpdateAchievementsRecentUnlockAndAlmostThere(); +void UpdateAchievementsPauseScreenInfo(); /// Switches to the achievements window. void SwitchToAchievements();