From e38039873ebb4c063ea880ba6f56cc96ed7465b2 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 8 Aug 2025 23:10:31 +1000 Subject: [PATCH] Achievements: Infer and display beaten/completed time --- dep/rcheevos/include/rc_client.h | 3 +++ dep/rcheevos/src/rc_client.c | 40 ++++++++++++++++++++++++++++++++ src/core/achievements.cpp | 38 +++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/dep/rcheevos/include/rc_client.h b/dep/rcheevos/include/rc_client.h index 2ec438c02..f96570e59 100644 --- a/dep/rcheevos/include/rc_client.h +++ b/dep/rcheevos/include/rc_client.h @@ -216,6 +216,9 @@ typedef struct rc_client_user_game_summary_t { uint32_t points_core; uint32_t points_unlocked; + + time_t beaten_time; /* 0 if not beaten, otherwise the time the game was beaten */ + time_t completed_time; /* 0 if not mastered, otherwise the time the game was mastered */ } rc_client_user_game_summary_t; /** diff --git a/dep/rcheevos/src/rc_client.c b/dep/rcheevos/src/rc_client.c index 291b1af86..8550712d9 100644 --- a/dep/rcheevos/src/rc_client.c +++ b/dep/rcheevos/src/rc_client.c @@ -923,6 +923,10 @@ static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t { rc_client_achievement_info_t* achievement = subset->achievements; rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements; + uint32_t num_progression_achievements = 0, unlocked_progression_achievements = 0; + uint32_t num_win_condition_achievements = 0; + time_t last_progression_unlock = 0, first_win_condition_unlock = 0; + time_t last_achievement_unlock = 0; for (; achievement < stop; ++achievement) { switch (achievement->public_.category) { case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE: @@ -932,6 +936,29 @@ static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t if (achievement->public_.unlocked & unlock_bit) { ++summary->num_unlocked_achievements; summary->points_unlocked += achievement->public_.points; + + last_achievement_unlock = (achievement->public_.unlock_time > last_achievement_unlock) ? + achievement->public_.unlock_time : + last_achievement_unlock; + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) + { + ++unlocked_progression_achievements; + last_progression_unlock = (achievement->public_.unlock_time > last_progression_unlock) ? + achievement->public_.unlock_time : + last_progression_unlock; + } + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) + { + ++num_win_condition_achievements; + first_win_condition_unlock = (first_win_condition_unlock == 0) ? + achievement->public_.unlock_time : + first_win_condition_unlock; + } + } else { + if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION) + ++num_progression_achievements; + else if (achievement->public_.type == RC_CLIENT_ACHIEVEMENT_TYPE_WIN) + ++num_win_condition_achievements; } if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { ++summary->num_unsupported_achievements; @@ -947,6 +974,19 @@ static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t continue; } } + + /* Game considered beaten when all progression achievements are unlocked and any win condition achievement + * is unlocked, or all progression achievements are unlocked and no there are no win condition achievements. */ + summary->beaten_time = 0; + if (num_progression_achievements > 0 && unlocked_progression_achievements == num_progression_achievements && + (num_win_condition_achievements == 0 || first_win_condition_unlock > 0)) { + summary->beaten_time = (num_win_condition_achievements == 0) ? last_progression_unlock : first_win_condition_unlock; + } + + /* Game considered completed when all achievements are unlocked */ + summary->completed_time = 0; + if (summary->num_unlocked_achievements == summary->num_core_achievements) + summary->completed_time = last_achievement_unlock; } void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary) diff --git a/src/core/achievements.cpp b/src/core/achievements.cpp index 7cc5efb8a..66a45b482 100644 --- a/src/core/achievements.cpp +++ b/src/core/achievements.cpp @@ -2769,7 +2769,8 @@ void Achievements::DrawAchievementsWindow() static constexpr float alpha = 0.8f; static constexpr float heading_alpha = 0.95f; - static constexpr float heading_height_unscaled = 110.0f; + const float heading_height_unscaled = + (s_state.game_summary.beaten_time > 0 || s_state.game_summary.completed_time) ? 140.0f : 110.0f; const ImVec4 background = ImGuiFullscreen::ModAlpha(UIStyle.BackgroundColor, alpha); const ImVec4 heading_background = ImGuiFullscreen::ModAlpha(UIStyle.BackgroundColor, heading_alpha); @@ -2845,6 +2846,41 @@ void Achievements::DrawAchievementsWindow() ImGui::GetColorU32(ImGuiFullscreen::DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])), text, nullptr, ImVec2(0.0f, 0.0f), 0.0f, &summary_bb); + if (s_state.game_summary.beaten_time > 0 || s_state.game_summary.completed_time > 0) + { + text.clear(); + if (s_state.game_summary.beaten_time > 0) + { + const std::string beaten_time = + Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast(s_state.game_summary.beaten_time)); + if (s_state.game_summary.completed_time > 0) + { + const std::string completion_time = + Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast(s_state.game_summary.beaten_time)); + text.format(TRANSLATE_FS("Achievements", "Game was beaten on {0}, and completed on {1}."), beaten_time, + completion_time); + } + else + { + text.format(TRANSLATE_FS("Achievements", "Game was beaten on {0}."), beaten_time); + } + } + else + { + const std::string completion_time = + Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast(s_state.game_summary.beaten_time)); + text.format(TRANSLATE_FS("Achievements", "Game was completed on {0}."), completion_time); + } + + const ImRect beaten_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize)); + RenderShadowedTextClipped( + UIStyle.Font, UIStyle.MediumFontSize, UIStyle.BoldFontWeight, beaten_bb.Min, beaten_bb.Max, + ImGui::GetColorU32(ImGuiFullscreen::DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text])), text, nullptr, + ImVec2(0.0f, 0.0f), 0.0f, &beaten_bb); + + top += UIStyle.MediumFontSize + spacing; + } + if (s_state.game_summary.num_core_achievements > 0) { const float progress_height = LayoutScale(20.0f);