@@ -155,6 +155,7 @@ static void BeginChangeDisc();
155155static void UpdateGameSummary (bool update_progress_database, bool force_update_progress_database);
156156static std::string GetLocalImagePath (const std::string_view image_name, int type);
157157static void DownloadImage (std::string url, std::string cache_path);
158+ static const std::string& GetCachedAchievementBadgePath (const rc_client_achievement_t * achievement, int state);
158159static void UpdateGlyphRanges ();
159160
160161static TinyString DecryptLoginToken (std::string_view encrypted_token, std::string_view username);
@@ -266,7 +267,10 @@ struct State
266267 rc_client_async_handle_t * load_game_request = nullptr ;
267268
268269 rc_client_achievement_list_t * achievement_list = nullptr ;
269- std::vector<std::pair<const void *, std::string>> achievement_badge_paths;
270+ std::vector<std::tuple<const void *, int , std::string>> achievement_badge_paths;
271+
272+ const rc_client_achievement_t * most_recent_unlock = nullptr ;
273+ const rc_client_achievement_t * achievement_nearest_completion = nullptr ;
270274
271275 rc_client_leaderboard_list_t * leaderboard_list = nullptr ;
272276 const rc_client_leaderboard_t * open_leaderboard = nullptr ;
@@ -1032,6 +1036,49 @@ void Achievements::UpdateGameSummary(bool update_progress_database, bool force_u
10321036 UpdateProgressDatabase (force_update_progress_database);
10331037}
10341038
1039+ void Achievements::UpdateRecentUnlockAndAlmostThere ()
1040+ {
1041+ const auto lock = GetLock ();
1042+ if (!IsActive ())
1043+ return ;
1044+
1045+ s_state.most_recent_unlock = nullptr ;
1046+ s_state.achievement_nearest_completion = nullptr ;
1047+
1048+ rc_client_achievement_list_t * const achievements = rc_client_create_achievement_list (
1049+ s_state.client , RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL , RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS );
1050+ if (!achievements)
1051+ return ;
1052+
1053+ for (u32 i = 0 ; i < achievements->num_buckets ; i++)
1054+ {
1055+ const rc_client_achievement_bucket_t & bucket = achievements->buckets [i];
1056+ for (u32 j = 0 ; j < bucket.num_achievements ; j++)
1057+ {
1058+ const rc_client_achievement_t * achievement = bucket.achievements [j];
1059+
1060+ if (achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED )
1061+ {
1062+ if (!s_state.most_recent_unlock || achievement->unlock_time > s_state.most_recent_unlock ->unlock_time )
1063+ s_state.most_recent_unlock = achievement;
1064+ }
1065+ else
1066+ {
1067+ // find the achievement with the greatest normalized progress, but skip anything below 80%,
1068+ // matching the rc_client definition of "almost there"
1069+ const float percent_cutoff = 80 .0f ;
1070+ if (achievement->measured_percent >= percent_cutoff &&
1071+ (!s_state.achievement_nearest_completion ||
1072+ achievement->measured_percent > s_state.achievement_nearest_completion ->measured_percent ))
1073+ {
1074+ s_state.achievement_nearest_completion = achievement;
1075+ }
1076+ }
1077+ }
1078+ }
1079+ rc_client_destroy_achievement_list (achievements);
1080+ }
1081+
10351082void Achievements::UpdateRichPresence (std::unique_lock<std::recursive_mutex>& lock)
10361083{
10371084 // Limit rich presence updates to once per second, since it could change per frame.
@@ -1973,6 +2020,18 @@ std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t*
19732020 return path;
19742021}
19752022
2023+ const std::string& Achievements::GetCachedAchievementBadgePath (const rc_client_achievement_t * achievement, int state)
2024+ {
2025+ for (const auto & [l_cheevo, l_state, l_path] : s_state.achievement_badge_paths )
2026+ {
2027+ if (l_cheevo == achievement && l_state == state)
2028+ return l_path;
2029+ }
2030+
2031+ std::string path = GetAchievementBadgePath (achievement, state);
2032+ return std::get<2 >(s_state.achievement_badge_paths .emplace_back (achievement, state, std::move (path)));
2033+ }
2034+
19762035std::string Achievements::GetLeaderboardUserBadgePath (const rc_client_leaderboard_entry_t * entry)
19772036{
19782037 // TODO: maybe we should just cache these in memory...
@@ -2314,6 +2373,9 @@ void Achievements::ClearUIState()
23142373 rc_client_destroy_achievement_list (s_state.achievement_list );
23152374 s_state.achievement_list = nullptr ;
23162375 }
2376+
2377+ s_state.most_recent_unlock = nullptr ;
2378+ s_state.achievement_nearest_completion = nullptr ;
23172379}
23182380
23192381template <typename T>
@@ -2485,83 +2547,203 @@ void Achievements::DrawGameOverlays()
24852547
24862548#ifndef __ANDROID__
24872549
2488- void Achievements::DrawPauseMenuOverlays ()
2550+ void Achievements::DrawPauseMenuOverlays (float start_pos_y )
24892551{
2552+ using ImGuiFullscreen::DarkerColor;
24902553 using ImGuiFullscreen::LayoutScale;
2554+ using ImGuiFullscreen::ModAlpha;
24912555 using ImGuiFullscreen::UIStyle;
24922556
2493- if (!HasActiveGame ())
2557+ if (!HasActiveGame () || ! HasAchievements () )
24942558 return ;
24952559
24962560 const auto lock = GetLock ();
24972561
2498- if (s_state.active_challenge_indicators .empty () && !s_state.active_progress_indicator .has_value ())
2499- return ;
2562+ const ImVec2& display_size = ImGui::GetIO ().DisplaySize ;
2563+ const float box_margin = LayoutScale (20 .0f );
2564+ const float box_width = LayoutScale (450 .0f );
2565+ const float box_padding = LayoutScale (15 .0f );
2566+ const float box_content_width = box_width - box_padding - box_padding;
2567+ const float box_rounding = LayoutScale (20 .0f );
2568+ const u32 box_background_color = ImGui::GetColorU32 (ModAlpha (UIStyle.BackgroundColor , 0 .8f ));
2569+ const ImU32 title_text_color = ImGui::GetColorU32 (UIStyle.BackgroundTextColor ) | IM_COL32_A_MASK ;
2570+ const ImU32 text_color = ImGui::GetColorU32 (DarkerColor (UIStyle.BackgroundTextColor )) | IM_COL32_A_MASK ;
2571+ const float paragraph_spacing = LayoutScale (10 .0f );
2572+ const float text_spacing = LayoutScale (2 .0f );
2573+
2574+ const float progress_height = LayoutScale (20 .0f );
2575+ const float badge_size = LayoutScale (40 .0f );
2576+ const float badge_text_width = box_content_width - badge_size - text_spacing - text_spacing;
25002577
2501- const ImGuiIO& io = ImGui::GetIO ();
2502- ImFont* font = UIStyle.MediumFont ;
2578+ ImDrawList* dl = ImGui::GetBackgroundDrawList ();
25032579
2504- const ImVec2 image_size (LayoutScale (ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY ,
2505- ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY ));
2506- const float start_y =
2507- LayoutScale (10 .0f + 4 .0f + 4 .0f ) + UIStyle.LargeFont ->FontSize + (UIStyle.MediumFont ->FontSize * 2 .0f );
2508- const float margin = LayoutScale (10 .0f );
2509- const float spacing = LayoutScale (10 .0f );
2510- const float padding = LayoutScale (10 .0f );
2580+ const auto get_achievement_height = [&badge_size, &badge_text_width, &text_spacing, &progress_height](
2581+ const rc_client_achievement_t * achievement, bool show_measured) {
2582+ const ImVec2 description_size = UIStyle.MediumFont ->CalcTextSizeA (UIStyle.MediumFont ->FontSize , FLT_MAX ,
2583+ badge_text_width, achievement->description );
2584+ float text_height = UIStyle.MediumFont ->FontSize + text_spacing + description_size.y ;
2585+ #if 0
2586+ if (show_measured && achievement->measured_percent > 0.0f)
2587+ text_height += text_spacing + progress_height;
2588+ #endif
25112589
2512- const float max_text_width = ImGuiFullscreen::LayoutScale (300 .0f );
2513- const float row_width = max_text_width + padding + padding + image_size.x + spacing;
2514- const float title_height = padding + font->FontSize + padding;
2590+ return std::max (text_height, badge_size);
2591+ };
25152592
2516- if (!s_state.active_challenge_indicators .empty ())
2593+ float box_height =
2594+ box_padding + box_padding + UIStyle.MediumFont ->FontSize + paragraph_spacing + progress_height + paragraph_spacing;
2595+ if (s_state.most_recent_unlock )
25172596 {
2518- const ImVec2 box_min (io.DisplaySize .x - row_width - margin, start_y + margin);
2519- const ImVec2 box_max (box_min.x + row_width,
2520- box_min.y + title_height +
2521- (static_cast <float >(s_state.active_challenge_indicators .size ()) * (image_size.y + padding)));
2597+ box_height += UIStyle.MediumFont ->FontSize + paragraph_spacing +
2598+ get_achievement_height (s_state.most_recent_unlock , false ) +
2599+ (s_state.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0 .0f );
2600+ }
2601+ if (s_state.achievement_nearest_completion )
2602+ {
2603+ box_height += UIStyle.MediumFont ->FontSize + paragraph_spacing +
2604+ get_achievement_height (s_state.achievement_nearest_completion , true );
2605+ }
25222606
2523- ImDrawList* dl = ImGui::GetBackgroundDrawList ( );
2524- dl-> AddRectFilled (box_min, box_max, IM_COL32 ( 0x21 , 0x21 , 0x21 , 200 ), LayoutScale ( 10 . 0f ) );
2525- dl-> AddText (font, font-> FontSize , ImVec2 (box_min.x + padding , box_min.y + padding), IM_COL32 ( 255 , 255 , 255 , 255 ),
2526- TRANSLATE ( " Achievements " , " Active Challenge Achievements " )) ;
2607+ ImVec2 box_min = ImVec2 (display_size. x - box_width - box_margin, start_pos_y + box_margin );
2608+ ImVec2 box_max = ImVec2 (box_min. x + box_width, box_min. y + box_height );
2609+ ImVec2 text_pos = ImVec2 (box_min.x + box_padding , box_min.y + box_padding);
2610+ ImVec2 text_size ;
25272611
2528- const float y_advance = image_size.y + spacing;
2529- const float acheivement_name_offset = (image_size.y - font->FontSize ) / 2 .0f ;
2530- const float max_non_ellipised_text_width = max_text_width - LayoutScale (10 .0f );
2531- ImVec2 position (box_min.x + padding, box_min.y + title_height);
2612+ dl->AddRectFilled (box_min, box_max, box_background_color, box_rounding);
25322613
2533- for (const AchievementChallengeIndicator& indicator : s_state.active_challenge_indicators )
2614+ const auto draw_achievement_with_summary = [&box_max, &badge_text_width, &dl, &title_text_color, &text_color,
2615+ &text_spacing, ¶graph_spacing, &text_pos, &progress_height,
2616+ &badge_size](const rc_client_achievement_t * achievement,
2617+ bool show_measured) {
2618+ const ImVec2 image_max = ImVec2 (text_pos.x + badge_size, text_pos.y + badge_size);
2619+ ImVec2 badge_text_pos = ImVec2 (image_max.x + text_spacing + text_spacing, text_pos.y );
2620+ const ImVec4 clip_rect = ImVec4 (badge_text_pos.x , badge_text_pos.y , badge_text_pos.x + badge_text_width, box_max.y );
2621+ const ImVec2 description_size = UIStyle.MediumFont ->CalcTextSizeA (UIStyle.MediumFont ->FontSize , FLT_MAX ,
2622+ badge_text_width, achievement->description );
2623+
2624+ GPUTexture* badge_tex = ImGuiFullscreen::GetCachedTextureAsync (
2625+ GetCachedAchievementBadgePath (achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED ));
2626+ dl->AddImage (badge_tex, text_pos, image_max);
2627+
2628+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , badge_text_pos, title_text_color, achievement->title ,
2629+ nullptr , 0 .0f , &clip_rect);
2630+ badge_text_pos.y += UIStyle.MediumFont ->FontSize + text_spacing;
2631+
2632+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , badge_text_pos, text_color, achievement->description ,
2633+ nullptr , badge_text_width, &clip_rect);
2634+ badge_text_pos.y += description_size.y ;
2635+
2636+ if (show_measured && achievement->measured_percent > 0 .0f )
25342637 {
2535- GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync (indicator.badge_path );
2536- if (!badge)
2537- continue ;
2638+ #if 0
2639+ // not a fan of the way this looks
2640+ badge_text_pos.y += text_spacing;
2641+
2642+ const float progress_fraction = static_cast<float>(achievement->measured_percent) / 100.0f;
2643+ const ImRect progress_bb(badge_text_pos, badge_text_pos + ImVec2(badge_text_width, progress_height));
2644+ const u32 progress_color = ImGui::GetColorU32(DarkerColor(UIStyle.SecondaryColor));
2645+ dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(UIStyle.PrimaryDarkColor));
2646+ dl->AddRectFilled(progress_bb.Min,
2647+ ImVec2(progress_bb.Min.x + progress_fraction * progress_bb.GetWidth(), progress_bb.Max.y),
2648+ progress_color);
2649+ const ImVec2 text_size =
2650+ UIStyle.MediumFont->CalcTextSizeA(UIStyle.MediumFont->FontSize, FLT_MAX, 0.0f, achievement->measured_progress);
2651+ dl->AddText(UIStyle.MediumFont, UIStyle.MediumFont->FontSize,
2652+ ImVec2(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f),
2653+ progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f)),
2654+ text_color, achievement->measured_progress);
2655+ #endif
2656+ }
25382657
2539- dl->AddImage (badge, position, position + image_size);
2658+ text_pos.y = badge_text_pos.y ;
2659+ };
25402660
2541- const char * achievement_title = indicator.achievement ->title ;
2542- const char * achievement_title_end = achievement_title + std::strlen (indicator.achievement ->title );
2543- const char * remaining_text = nullptr ;
2544- const ImVec2 text_width (font->CalcTextSizeA (font->FontSize , max_non_ellipised_text_width, 0 .0f , achievement_title,
2545- achievement_title_end, &remaining_text));
2546- const ImVec2 text_position (position.x + image_size.x + spacing, position.y + acheivement_name_offset);
2547- const ImVec4 text_bbox (text_position.x , text_position.y , text_position.x + max_text_width,
2548- text_position.y + image_size.y );
2549- const u32 text_color = IM_COL32 (255 , 255 , 255 , 255 );
2661+ TinyString buffer;
25502662
2551- if (remaining_text < achievement_title_end)
2552- {
2553- dl->AddText (font, font->FontSize , text_position, text_color, achievement_title, remaining_text, 0 .0f ,
2554- &text_bbox);
2555- dl->AddText (font, font->FontSize , ImVec2 (text_position.x + text_width.x , text_position.y ), text_color, " ..." ,
2556- nullptr , 0 .0f , &text_bbox);
2557- }
2558- else
2559- {
2560- dl->AddText (font, font->FontSize , text_position, text_color, achievement_title, achievement_title_end, 0 .0f ,
2561- &text_bbox);
2562- }
2663+ // title
2664+ {
2665+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , text_pos, text_color,
2666+ TRANSLATE_DISAMBIG (" Achievements" , " Achievements Unlocked" , " Pause Menu" ));
2667+ const float unlocked_fraction = static_cast <float >(s_state.game_summary .num_unlocked_achievements ) /
2668+ static_cast <float >(s_state.game_summary .num_core_achievements );
2669+ buffer.format (" {}%" , static_cast <u32 >(std::ceil (unlocked_fraction * 100 .0f )));
2670+ text_size =
2671+ UIStyle.MediumFont ->CalcTextSizeA (UIStyle.MediumFont ->FontSize , FLT_MAX , 0 .0f , buffer.c_str (), buffer.end_ptr ());
2672+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize ,
2673+ ImVec2 (text_pos.x + (box_content_width - text_size.x ), text_pos.y ), text_color, buffer.c_str (),
2674+ buffer.end_ptr ());
2675+ text_pos.y += UIStyle.MediumFont ->FontSize + paragraph_spacing;
2676+
2677+ const ImRect progress_bb (text_pos, text_pos + ImVec2 (box_content_width, progress_height));
2678+ const u32 progress_color = ImGui::GetColorU32 (DarkerColor (UIStyle.SecondaryColor ));
2679+ dl->AddRectFilled (progress_bb.Min , progress_bb.Max , ImGui::GetColorU32 (UIStyle.PrimaryDarkColor ));
2680+ dl->AddRectFilled (progress_bb.Min ,
2681+ ImVec2 (progress_bb.Min .x + unlocked_fraction * progress_bb.GetWidth (), progress_bb.Max .y ),
2682+ progress_color);
2683+
2684+ buffer.format (" {}/{}" , s_state.game_summary .num_unlocked_achievements , s_state.game_summary .num_core_achievements );
2685+ text_size =
2686+ UIStyle.MediumFont ->CalcTextSizeA (UIStyle.MediumFont ->FontSize , FLT_MAX , 0 .0f , buffer.c_str (), buffer.end_ptr ());
2687+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize ,
2688+ ImVec2 (progress_bb.Min .x + ((progress_bb.Max .x - progress_bb.Min .x ) / 2 .0f ) - (text_size.x / 2 .0f ),
2689+ progress_bb.Min .y + ((progress_bb.Max .y - progress_bb.Min .y ) / 2 .0f ) - (text_size.y / 2 .0f )),
2690+ text_color, buffer.c_str (), buffer.end_ptr ());
2691+ text_pos.y += progress_height + paragraph_spacing;
2692+ }
2693+
2694+ if (s_state.most_recent_unlock )
2695+ {
2696+ buffer.format (ICON_FA_LOCK_OPEN " {}" , TRANSLATE_DISAMBIG_SV (" Achievements" , " Most Recent" , " Pause Menu" ));
2697+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , text_pos, text_color, buffer.c_str (),
2698+ buffer.end_ptr ());
2699+ text_pos.y += UIStyle.MediumFont ->FontSize + paragraph_spacing;
2700+
2701+ draw_achievement_with_summary (s_state.most_recent_unlock , false );
2702+
2703+ // extra spacing if we have two
2704+ text_pos.y += s_state.achievement_nearest_completion ? (paragraph_spacing + paragraph_spacing) : 0 .0f ;
2705+ }
2706+
2707+ if (s_state.achievement_nearest_completion )
2708+ {
2709+ buffer.format (ICON_FA_LOCK " {}" , TRANSLATE_DISAMBIG_SV (" Achievements" , " Nearest Completion" , " Pause Menu" ));
2710+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , text_pos, text_color, buffer.c_str (),
2711+ buffer.end_ptr ());
2712+ text_pos.y += UIStyle.MediumFont ->FontSize + paragraph_spacing;
2713+
2714+ draw_achievement_with_summary (s_state.achievement_nearest_completion , true );
2715+ text_pos.y += paragraph_spacing;
2716+ }
2717+
2718+ // Challenge indicators
25632719
2564- position.y += y_advance;
2720+ if (!s_state.active_challenge_indicators .empty ())
2721+ {
2722+ box_height = box_padding + box_padding + UIStyle.MediumFont ->FontSize ;
2723+ for (size_t i = 0 ; i < s_state.active_challenge_indicators .size (); i++)
2724+ {
2725+ const AchievementChallengeIndicator& indicator = s_state.active_challenge_indicators [i];
2726+ box_height += paragraph_spacing + get_achievement_height (indicator.achievement , false ) +
2727+ ((i == s_state.active_challenge_indicators .size () - 1 ) ? paragraph_spacing : 0 .0f );
2728+ }
2729+
2730+ box_min = ImVec2 (box_min.x , box_max.y + box_margin);
2731+ box_max = ImVec2 (box_min.x + box_width, box_min.y + box_height);
2732+ text_pos = ImVec2 (box_min.x + box_padding, box_min.y + box_padding);
2733+
2734+ dl->AddRectFilled (box_min, box_max, box_background_color, box_rounding);
2735+
2736+ buffer.format (ICON_FA_STOPWATCH " {}" ,
2737+ TRANSLATE_DISAMBIG_SV (" Achievements" , " Active Challenge Achievements" , " Pause Menu" ));
2738+ dl->AddText (UIStyle.MediumFont , UIStyle.MediumFont ->FontSize , text_pos, text_color, buffer.c_str (),
2739+ buffer.end_ptr ());
2740+ text_pos.y += UIStyle.MediumFont ->FontSize ;
2741+
2742+ for (const AchievementChallengeIndicator& indicator : s_state.active_challenge_indicators )
2743+ {
2744+ text_pos.y += paragraph_spacing;
2745+ draw_achievement_with_summary (indicator.achievement , false );
2746+ text_pos.y += paragraph_spacing;
25652747 }
25662748 }
25672749}
@@ -2826,24 +3008,13 @@ void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo)
28263008 if (!visible)
28273009 return ;
28283010
2829- std::string* badge_path;
2830- if (const auto badge_it = std::find_if (s_state.achievement_badge_paths .begin (), s_state.achievement_badge_paths .end (),
2831- [cheevo](const auto & it) { return (it.first == cheevo); });
2832- badge_it != s_state.achievement_badge_paths .end ())
2833- {
2834- badge_path = &badge_it->second ;
2835- }
2836- else
2837- {
2838- std::string new_badge_path = Achievements::GetAchievementBadgePath (cheevo, cheevo->state );
2839- badge_path = &s_state.achievement_badge_paths .emplace_back (cheevo, std::move (new_badge_path)).second ;
2840- }
3011+ const std::string& badge_path = GetCachedAchievementBadgePath (cheevo, cheevo->state );
28413012
28423013 const ImVec2 image_size (
28433014 LayoutScale (ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT , ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT ));
2844- if (!badge_path-> empty ())
3015+ if (!badge_path. empty ())
28453016 {
2846- GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync (* badge_path);
3017+ GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync (badge_path);
28473018 if (badge)
28483019 {
28493020 ImGui::GetWindowDrawList ()->AddImage (badge, bb.Min , bb.Min + image_size, ImVec2 (0 .0f , 0 .0f ), ImVec2 (1 .0f , 1 .0f ),
0 commit comments