Skip to content

Commit e422afd

Browse files
committed
FullscreenUI: Improve achievements pause menu overlays
- Add most recent unlock/nearest completion. - Make it look nicer and better fit with the rest of the interface.
1 parent ba32959 commit e422afd

5 files changed

Lines changed: 251 additions & 76 deletions

File tree

dep/imgui/include/IconsEmoji.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@
2929
#define ICON_EMOJI_UNLOCKED "\xf0\x9f\x94\x93"
3030
#define ICON_EMOJI_REFRESH "\xf0\x9f\x94\x84"
3131
#define ICON_EMOJI_PROHIBITED "\xf0\x9f\x9a\xab"
32+
#define ICON_EMOJI_CALENDAR "\xF0\x9F\x93\x85"

src/core/achievements.cpp

Lines changed: 242 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ static void BeginChangeDisc();
155155
static void UpdateGameSummary(bool update_progress_database, bool force_update_progress_database);
156156
static std::string GetLocalImagePath(const std::string_view image_name, int type);
157157
static void DownloadImage(std::string url, std::string cache_path);
158+
static const std::string& GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, int state);
158159
static void UpdateGlyphRanges();
159160

160161
static 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+
10351082
void 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+
19762035
std::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

23192381
template<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, &paragraph_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

Comments
 (0)