FullscreenUI: Add achievement subset selection

This commit is contained in:
Stenzek
2025-12-07 18:20:13 +10:00
parent 75380e7859
commit ac0a299884
3 changed files with 313 additions and 39 deletions

View File

@@ -1394,16 +1394,7 @@ void Achievements::HandleSubsetCompleteEvent(const rc_client_event_t* event)
if (g_settings.achievements_notifications && event->subset->badge_name[0] != '\0')
{
// Need to grab the icon for the subset.
std::string badge_path = GetLocalImagePath(event->subset->badge_name, RC_IMAGE_TYPE_GAME);
if (!FileSystem::FileExists(badge_path.c_str()))
{
std::string url;
if (IsUsingRAIntegration() || !event->subset->badge_url)
url = GetImageURL(event->subset->badge_name, RC_IMAGE_TYPE_GAME);
else
url = event->subset->badge_url;
DownloadImage(std::move(url), badge_path);
}
std::string badge_path = GetSubsetBadgePath(event->subset);
std::string message = fmt::format(
TRANSLATE_FS("Achievements", "Subset complete.\n{0}, {1}."),
@@ -1873,6 +1864,22 @@ std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboar
return path;
}
std::string Achievements::GetSubsetBadgePath(const rc_client_subset_t* subset)
{
std::string badge_path = GetLocalImagePath(subset->badge_name, RC_IMAGE_TYPE_GAME);
if (!FileSystem::FileExists(badge_path.c_str()))
{
std::string url;
if (IsUsingRAIntegration() || !subset->badge_url)
url = GetImageURL(subset->badge_name, RC_IMAGE_TYPE_GAME);
else
url = subset->badge_url;
DownloadImage(std::move(url), badge_path);
}
return badge_path;
}
bool Achievements::IsLoggedIn()
{
return (rc_client_get_user_info(s_state.client) != nullptr);

View File

@@ -32,6 +32,8 @@ std::string GetAchievementBadgePath(const rc_client_achievement_t* achievement,
bool download_if_missing = true);
std::string GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry);
std::string GetSubsetBadgePath(const rc_client_subset_t* subset);
} // namespace Achievements
#ifndef __ANDROID__

View File

@@ -10,6 +10,7 @@
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/timer.h"
#include "IconsEmoji.h"
@@ -62,6 +63,15 @@ struct PauseMenuTimedMeasuredAchievementInfo : PauseMenuMeasuredAchievementInfo
} // namespace
static void AddSubsetInfo(const rc_client_subset_t* subset);
static bool IsCoreSubsetOpen();
static void SetCurrentSubsetID(u32 subset_id);
static void DrawSubsetSelector();
template<typename T>
static void CollectSubsetsFromList(const T* list);
template<typename T>
static bool IsBucketVisibleInCurrentSubset(const T& bucket);
static const std::string& GetCachedAchievementBadgePath(const rc_client_achievement_t* achievement, bool locked);
template<typename T>
@@ -85,8 +95,22 @@ static void DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, u32
namespace {
struct SubsetInfo
{
std::string full_name;
std::string short_name;
std::string badge_path;
u32 subset_id;
u32 num_leaderboards;
rc_client_user_game_summary_t summary;
};
struct AchievementsLocals
{
// Shared by both achievements and leaderboards, TODO: add all filter
std::vector<SubsetInfo> subset_info_list;
const SubsetInfo* open_subset = nullptr;
rc_client_achievement_list_t* achievement_list = nullptr;
std::vector<std::tuple<const void*, std::string, bool>> achievement_badge_paths;
@@ -135,6 +159,9 @@ void FullscreenUI::ClearAchievementsState()
s_achievements_locals.achievement_list = nullptr;
}
s_achievements_locals.open_subset = nullptr;
s_achievements_locals.subset_info_list.clear();
s_achievements_locals.most_recent_unlock.reset();
s_achievements_locals.achievement_nearest_completion.reset();
}
@@ -581,6 +608,159 @@ void FullscreenUI::OpenAchievementsWindow()
});
}
void FullscreenUI::AddSubsetInfo(const rc_client_subset_t* subset)
{
const std::string_view game_title = Achievements::GetGameTitle();
SubsetInfo info;
info.subset_id = subset->id;
// is this the core subset?
std::string_view subset_title = subset->title;
if (game_title == subset_title)
{
info.full_name = subset_title;
info.short_name = TRANSLATE_STR("Achievements", "Core");
}
else if (subset_title.starts_with(game_title))
{
info.full_name = subset_title;
info.short_name = StringUtil::StripWhitespace(subset_title.substr(game_title.size()));
if (info.short_name.starts_with("-"))
info.short_name = StringUtil::StripWhitespace(info.short_name.substr(1));
if (info.short_name.empty())
info.short_name = StringUtil::ToChars(subset->id);
}
else
{
info.full_name = fmt::format(TRANSLATE_FS("Achievements", "{0} - {1}"), game_title, subset_title);
info.short_name = subset_title;
}
info.badge_path = Achievements::GetSubsetBadgePath(subset);
info.num_leaderboards = subset->num_leaderboards;
info.summary = {};
rc_client_get_user_subset_summary(Achievements::GetClient(), subset->id, &info.summary);
s_achievements_locals.subset_info_list.push_back(std::move(info));
}
void FullscreenUI::DrawSubsetSelector()
{
DebugAssert(s_achievements_locals.open_subset);
const float& font_size = UIStyle.MediumFontSize;
constexpr float font_weight = UIStyle.BoldFontWeight;
constexpr float nav_x_padding = 12.0f;
constexpr float nav_y_padding = 8.0f;
float nav_width = 0.0f;
for (const SubsetInfo& subset : s_achievements_locals.subset_info_list)
nav_width += CalcFloatingNavBarButtonWidth(subset.short_name, font_size, font_size, font_weight, nav_x_padding);
BeginFloatingNavBar(30.0f, 10.0f, nav_width, font_size, 1.0f, 0.0f, nav_x_padding, nav_y_padding);
std::optional<u32> new_subset_id;
for (size_t i = 0; i < s_achievements_locals.subset_info_list.size(); i++)
{
const SubsetInfo& subset = s_achievements_locals.subset_info_list[i];
GPUTexture* badge = GetCachedTextureAsync(subset.badge_path);
if (FloatingNavBarIcon(subset.short_name, badge, (&subset == s_achievements_locals.open_subset), font_size,
font_size, font_weight))
{
new_subset_id = subset.subset_id;
}
}
if (!new_subset_id.has_value())
{
const size_t i = s_achievements_locals.open_subset - &s_achievements_locals.subset_info_list[0];
if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft, true) ||
ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, true) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow, true))
{
new_subset_id = (i == 0) ? s_achievements_locals.subset_info_list.back().subset_id :
s_achievements_locals.subset_info_list[i - 1].subset_id;
}
else if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight, true) ||
ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, true) || ImGui::IsKeyPressed(ImGuiKey_RightArrow, true))
{
new_subset_id = ((i + 1) == s_achievements_locals.subset_info_list.size()) ?
s_achievements_locals.subset_info_list.front().subset_id :
s_achievements_locals.subset_info_list[i + 1].subset_id;
}
}
EndFloatingNavBar();
if (new_subset_id.has_value())
{
BeginTransition(DEFAULT_TRANSITION_TIME,
[new_subset_id = new_subset_id.value()]() { SetCurrentSubsetID(new_subset_id); });
}
}
bool FullscreenUI::IsCoreSubsetOpen()
{
return (!s_achievements_locals.open_subset ||
s_achievements_locals.open_subset == &s_achievements_locals.subset_info_list[0]);
}
void FullscreenUI::SetCurrentSubsetID(u32 subset_id)
{
for (const SubsetInfo& info : s_achievements_locals.subset_info_list)
{
if (info.subset_id == subset_id)
{
s_achievements_locals.open_subset = &info;
QueueResetFocus(FocusResetType::ViewChanged);
return;
}
}
}
template<typename T>
void FullscreenUI::CollectSubsetsFromList(const T* list)
{
s_achievements_locals.open_subset = nullptr;
s_achievements_locals.subset_info_list.clear();
if (std::any_of(list->buckets, list->buckets + list->num_buckets,
[&list](const auto& it) { return it.subset_id != list->buckets[0].subset_id; }))
{
for (u32 bucket_idx = 0; bucket_idx < list->num_buckets; bucket_idx++)
{
const auto& bucket = list->buckets[bucket_idx];
if (std::ranges::none_of(s_achievements_locals.subset_info_list,
[&bucket](const SubsetInfo& info) { return info.subset_id == bucket.subset_id; }))
{
const rc_client_subset_t* subset = rc_client_get_subset_info(Achievements::GetClient(), bucket.subset_id);
if (subset)
AddSubsetInfo(subset);
}
}
// hopefully the first will be core...
Assert(s_achievements_locals.subset_info_list.size() > 1);
s_achievements_locals.open_subset = &s_achievements_locals.subset_info_list[0];
}
}
template<typename T>
bool FullscreenUI::IsBucketVisibleInCurrentSubset(const T& bucket)
{
// Show e.g. active challenges/leaderboards in all subsets.
if (bucket.subset_id == 0)
return true;
if (s_achievements_locals.open_subset && bucket.subset_id != s_achievements_locals.open_subset->subset_id)
return false;
return true;
}
void FullscreenUI::SwitchToAchievements()
{
const auto lock = Achievements::GetLock();
@@ -617,6 +797,7 @@ void FullscreenUI::SwitchToAchievements()
}
}
CollectSubsetsFromList(s_achievements_locals.achievement_list);
SwitchToMainWindow(MainWindowType::Achievements);
}
@@ -631,7 +812,9 @@ void FullscreenUI::DrawAchievementsWindow()
return;
}
const rc_client_user_game_summary_t& summary = Achievements::GetGameSummary();
const rc_client_user_game_summary_t& summary =
s_achievements_locals.open_subset ? s_achievements_locals.open_subset->summary : Achievements::GetGameSummary();
const bool is_core_subset = IsCoreSubsetOpen();
const float heading_height_unscaled = ((summary.beaten_time > 0 || summary.completed_time) ? 122.0f : 102.0f) +
((summary.num_unsupported_achievements > 0) ? 20.0f : 0.0f);
@@ -667,10 +850,16 @@ void FullscreenUI::DrawAchievementsWindow()
SmallString text;
ImVec2 text_size;
if (s_achievements_locals.open_subset)
DrawSubsetSelector();
close_window = (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || WantsToCloseMenu());
const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize));
text.assign(Achievements::GetGameTitle());
if (s_achievements_locals.open_subset)
text.assign(s_achievements_locals.open_subset->full_name);
else
text.assign(Achievements::GetGameTitle());
if (rc_client_get_hardcore_enabled(Achievements::GetClient()))
text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)"));
@@ -700,7 +889,8 @@ void FullscreenUI::DrawAchievementsWindow()
}
else
{
text.format(ICON_FA_BAN " {}", TRANSLATE_SV("Achievements", "This game has no achievements."));
text.format(ICON_FA_BAN " {}", is_core_subset ? TRANSLATE_SV("Achievements", "This game has no achievements.") :
TRANSLATE_SV("Achievements", "This subset has no achievements."));
}
top += UIStyle.MediumFontSize + spacing;
@@ -737,19 +927,33 @@ void FullscreenUI::DrawAchievementsWindow()
{
const std::string completion_time =
Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast<s64>(summary.completed_time));
text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}, and completed on {1}."), beaten_time,
completion_time);
if (is_core_subset)
{
text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}, and completed on {1}."),
beaten_time, completion_time);
}
else
{
text.append_format(TRANSLATE_FS("Achievements", "Subset was beaten on {0}, and completed on {1}."),
beaten_time, completion_time);
}
}
else
{
text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}."), beaten_time);
if (is_core_subset)
text.append_format(TRANSLATE_FS("Achievements", "Game was beaten on {0}."), beaten_time);
else
text.append_format(TRANSLATE_FS("Achievements", "Subset was beaten on {0}."), beaten_time);
}
}
else
{
const std::string completion_time =
Host::FormatNumber(Host::NumberFormatType::ShortDate, static_cast<s64>(summary.completed_time));
text.append_format(TRANSLATE_FS("Achievements", "Game was completed on {0}."), completion_time);
if (is_core_subset)
text.append_format(TRANSLATE_FS("Achievements", "Game was completed on {0}."), completion_time);
else
text.append_format(TRANSLATE_FS("Achievements", "Subset was completed on {0}."), completion_time);
}
const ImRect beaten_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.MediumFontSize));
@@ -821,12 +1025,11 @@ void FullscreenUI::DrawAchievementsWindow()
for (u32 bucket_idx = 0; bucket_idx < s_achievements_locals.achievement_list->num_buckets; bucket_idx++)
{
const rc_client_achievement_bucket_t& bucket = s_achievements_locals.achievement_list->buckets[bucket_idx];
if (bucket.bucket_type != bucket_type)
if (bucket.bucket_type != bucket_type || !IsBucketVisibleInCurrentSubset(bucket))
continue;
DebugAssert(bucket.bucket_type < NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS);
// TODO: Once subsets are supported, this will need to change.
bool& bucket_collapsed = buckets_collapsed[bucket.bucket_type];
bucket_collapsed ^= MenuHeadingButton(
TinyString::from_format("{} {}", bucket_names[bucket.bucket_type].first,
@@ -854,17 +1057,39 @@ void FullscreenUI::DrawAchievementsWindow()
if (IsGamepadInputSource())
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
if (s_achievements_locals.open_subset)
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
}
else
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
}
}
else
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
if (s_achievements_locals.open_subset)
{
SetFullscreenFooterText(std::array{
std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
}
else
{
SetFullscreenFooterText(std::array{
std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "View Details")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
}
}
if (close_window)
@@ -1079,6 +1304,7 @@ void FullscreenUI::SwitchToLeaderboards()
return;
}
CollectSubsetsFromList(s_achievements_locals.leaderboard_list);
SwitchToMainWindow(MainWindowType::Leaderboards);
}
@@ -1129,7 +1355,10 @@ void FullscreenUI::DrawLeaderboardsWindow()
float top = heading_pos.y;
const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + UIStyle.LargeFontSize));
text.assign(Achievements::GetGameTitle());
if (s_achievements_locals.open_subset)
text.assign(s_achievements_locals.open_subset->full_name);
else
text.assign(Achievements::GetGameTitle());
top += UIStyle.LargeFontSize + spacing_small;
@@ -1156,8 +1385,17 @@ void FullscreenUI::DrawLeaderboardsWindow()
{
u32 count = 0;
for (u32 i = 0; i < s_achievements_locals.leaderboard_list->num_buckets; i++)
count += s_achievements_locals.leaderboard_list->buckets[i].num_leaderboards;
text = TRANSLATE_PLURAL_SSTR("Achievements", "This game has %n leaderboards.", "Leaderboard count", count);
{
const rc_client_leaderboard_bucket_t& bucket = s_achievements_locals.leaderboard_list->buckets[i];
if (IsBucketVisibleInCurrentSubset(bucket))
count += bucket.num_leaderboards;
}
if (IsCoreSubsetOpen())
text = TRANSLATE_PLURAL_SSTR("Achievements", "This game has %n leaderboards.", "Leaderboard count", count);
else
text = TRANSLATE_PLURAL_SSTR("Achievements", "This subset has %n leaderboards.", "Leaderboard count", count);
summary_color = ImGui::GetColorU32(DarkerColor(ImGui::GetStyle().Colors[ImGuiCol_Text]));
}
@@ -1185,6 +1423,9 @@ void FullscreenUI::DrawLeaderboardsWindow()
if (!is_leaderboard_open)
{
if (s_achievements_locals.open_subset)
DrawSubsetSelector();
if (FloatingButton(ICON_FA_XMARK, 10.0f, 10.0f, 1.0f, 0.0f, true) || WantsToCloseMenu())
ReturnToPreviousWindow();
}
@@ -1254,6 +1495,9 @@ void FullscreenUI::DrawLeaderboardsWindow()
for (u32 bucket_index = 0; bucket_index < s_achievements_locals.leaderboard_list->num_buckets; bucket_index++)
{
const rc_client_leaderboard_bucket_t& bucket = s_achievements_locals.leaderboard_list->buckets[bucket_index];
if (!IsBucketVisibleInCurrentSubset(bucket))
continue;
for (u32 i = 0; i < bucket.num_leaderboards; i++)
DrawLeaderboardListEntry(bucket.leaderboards[i]);
}
@@ -1264,19 +1508,40 @@ void FullscreenUI::DrawLeaderboardsWindow()
if (IsGamepadInputSource())
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
if (s_achievements_locals.open_subset)
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_LEFT_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
}
else
{
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_XBOX_DPAD_UP_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_BUTTON_A, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_BUTTON_B, TRANSLATE_SV("Achievements", "Back"))});
}
}
else
{
if (s_achievements_locals.open_subset)
{
SetFullscreenFooterText(std::array{
std::make_pair(ICON_PF_ARROW_LEFT ICON_PF_ARROW_RIGHT, TRANSLATE_SV("Achievements", "Change Subset")),
std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
}
else
{
SetFullscreenFooterText(std::array{
std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
}
}
SetFullscreenFooterText(
std::array{std::make_pair(ICON_PF_ARROW_UP ICON_PF_ARROW_DOWN, TRANSLATE_SV("Achievements", "Change Selection")),
std::make_pair(ICON_PF_ENTER, TRANSLATE_SV("Achievements", "Open Leaderboard")),
std::make_pair(ICON_PF_ESC, TRANSLATE_SV("Achievements", "Back"))});
}
else
{