Achievements: Use note area in more notifications

- Show RA logo for login/game summary.
- Add icons to game complete, subset complete, leaderboard start/fail.
This commit is contained in:
Stenzek
2026-01-16 01:17:40 +10:00
parent d02df7cf4b
commit 18768bcaba
5 changed files with 152 additions and 69 deletions

View File

@@ -51,3 +51,4 @@
#define ICON_EMOJI_BAR_CHART "\xf0\x9f\x93\x8a"
#define ICON_EMOJI_CHART_UPWARDS_TREND "\xf0\x9f\x93\x88"
#define ICON_EMOJI_DIRECT_HIT "\xf0\x9f\x8e\xaf"
#define ICON_EMOJI_RED_FLAG "\xf0\x9f\x9a\xa9"

View File

@@ -74,7 +74,6 @@ static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav
static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav";
static constexpr const char* CACHE_SUBDIRECTORY_NAME = "achievement_images";
constexpr const char* const RA_LOGO_ICON_NAME = "images/ra-icon.webp";
constexpr const std::string_view NOTIFICATION_SPINNER_NOTE = "__spinner__";
static constexpr float LOGIN_NOTIFICATION_TIME = 5.0f;
static constexpr float ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME = 5.0f;
@@ -85,7 +84,7 @@ static constexpr float CHALLENGE_STARTED_NOTIFICATION_TIME = 5.0f;
static constexpr float CHALLENGE_FAILED_NOTIFICATION_TIME = 5.0f;
static constexpr float LEADERBOARD_STARTED_NOTIFICATION_TIME = 3.0f;
static constexpr float LEADERBOARD_FAILED_NOTIFICATION_TIME = 3.0f;
static constexpr float LEADERBOARD_NOTIFICATION_MIN_WIDTH = 380.0f;
static constexpr u16 LEADERBOARD_NOTIFICATION_MIN_WIDTH = 380;
// Some API calls are really slow. Set a longer timeout.
static constexpr float SERVER_CALL_TIMEOUT = 60.0f;
@@ -1303,7 +1302,8 @@ void Achievements::DisplayAchievementSummary()
FullscreenUI::AddAchievementNotification("AchievementsSummary",
IsHardcoreModeActive() ? ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME_HC :
ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME,
s_state.game_icon, s_state.game_title, std::string(summary), {});
s_state.game_icon, s_state.game_title, std::string(summary),
RA_LOGO_ICON_NAME, FullscreenUI::AchievementNotificationNoteType::Image);
if (s_state.game_summary.num_unsupported_achievements > 0)
{
@@ -1311,8 +1311,7 @@ void Achievements::DisplayAchievementSummary()
"UnsupportedAchievements", ACHIEVEMENT_SUMMARY_UNSUPPORTED_TIME, "images/warning.svg",
TRANSLATE_STR("Achievements", "Unsupported Achievements"),
TRANSLATE_PLURAL_STR("Achievements", "%n achievements are not supported by DuckStation.", "Achievement popup",
s_state.game_summary.num_unsupported_achievements),
{});
s_state.game_summary.num_unsupported_achievements));
}
}
@@ -1365,10 +1364,12 @@ void Achievements::HandleUnlockEvent(const rc_client_event_t* event)
if (cheevo->points > 0)
note = fmt::format(ICON_EMOJI_TROPHY " {}", cheevo->points);
FullscreenUI::AddAchievementNotification(fmt::format("achievement_unlock_{}", cheevo->id),
static_cast<float>(g_settings.achievements_notification_duration),
GetAchievementBadgePath(cheevo, false), std::move(title),
std::string(cheevo->description), std::move(note));
FullscreenUI::AddAchievementNotification(
fmt::format("achievement_unlock_{}", cheevo->id),
static_cast<float>(g_settings.achievements_notification_duration), GetAchievementBadgePath(cheevo, false),
std::move(title), std::string(cheevo->description), std::move(note),
(cheevo->points > 0) ? FullscreenUI::AchievementNotificationNoteType::Text :
FullscreenUI::AchievementNotificationNoteType::None);
}
if (g_settings.achievements_sound_effects)
@@ -1383,14 +1384,14 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event)
if (g_settings.achievements_notifications)
{
std::string message = fmt::format(
TRANSLATE_FS("Achievements", "Game complete.\n{0}, {1}."),
TRANSLATE_FS("Achievements", "Game complete.\n{0} and {1}."),
TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup",
s_state.game_summary.num_unlocked_achievements),
TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked));
std::string note = fmt::format(ICON_EMOJI_TROPHY " {}", s_state.game_summary.points_unlocked);
FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, s_state.game_icon,
s_state.game_title, std::move(message), std::move(note));
s_state.game_title, std::move(message), ICON_EMOJI_TROPHY,
FullscreenUI::AchievementNotificationNoteType::IconText);
}
}
@@ -1405,14 +1406,14 @@ void Achievements::HandleSubsetCompleteEvent(const rc_client_event_t* event)
std::string badge_path = GetSubsetBadgePath(event->subset);
std::string message = fmt::format(
TRANSLATE_FS("Achievements", "Subset complete.\n{0}, {1}."),
TRANSLATE_FS("Achievements", "Subset complete.\n{0} and {1}."),
TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup",
s_state.game_summary.num_unlocked_achievements),
TRANSLATE_PLURAL_STR("Achievements", "%n points", "Achievement points", s_state.game_summary.points_unlocked));
FullscreenUI::AddAchievementNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME,
std::move(badge_path), std::string(event->subset->title),
std::move(message), {});
FullscreenUI::AddAchievementNotification(
"achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, std::move(badge_path), std::string(event->subset->title),
std::move(message), ICON_EMOJI_CHECKMARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText);
}
}
@@ -1424,7 +1425,8 @@ void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event)
{
FullscreenUI::AddAchievementNotification(
fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_STARTED_NOTIFICATION_TIME, s_state.game_icon,
std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt started."), {});
std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt started."),
ICON_EMOJI_RED_FLAG, FullscreenUI::AchievementNotificationNoteType::IconText);
}
}
@@ -1436,7 +1438,8 @@ void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event)
{
FullscreenUI::AddAchievementNotification(
fmt::format("leaderboard_{}", event->leaderboard->id), LEADERBOARD_FAILED_NOTIFICATION_TIME, s_state.game_icon,
std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt failed."), {});
std::string(event->leaderboard->title), TRANSLATE_STR("Achievements", "Leaderboard attempt failed."),
ICON_EMOJI_CROSS_MARK_BUTTON, FullscreenUI::AchievementNotificationNoteType::IconText);
}
}
@@ -1475,7 +1478,9 @@ void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* even
fmt::format("leaderboard_{}", event->leaderboard->id),
static_cast<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
std::string(event->leaderboard->title), std::move(message),
std::string(g_settings.achievements_spectator_mode ? std::string_view() : NOTIFICATION_SPINNER_NOTE),
g_settings.achievements_spectator_mode ? std::string(ICON_EMOJI_CHART_UPWARDS_TREND) : std::string(),
g_settings.achievements_spectator_mode ? FullscreenUI::AchievementNotificationNoteType::IconText :
FullscreenUI::AchievementNotificationNoteType::Spinner,
LEADERBOARD_NOTIFICATION_MIN_WIDTH);
}
@@ -1497,7 +1502,7 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve
};
std::string message = fmt::format(
"{} {}\n" ICON_EMOJI_BAR_CHART " {}", GetLeaderboardFormatIcon(event->leaderboard->format),
"{} {}\n" ICON_EMOJI_CHART_UPWARDS_TREND " {}", GetLeaderboardFormatIcon(event->leaderboard->format),
TinyString::from_format(
fmt::runtime(Host::TranslateToStringView(
"Achievements",
@@ -1506,11 +1511,11 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve
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),
static_cast<float>(g_settings.achievements_leaderboard_duration),
s_state.game_icon, std::string(event->leaderboard->title),
std::move(message), ICON_EMOJI_CHART_UPWARDS_TREND,
LEADERBOARD_NOTIFICATION_MIN_WIDTH);
FullscreenUI::AddAchievementNotification(
fmt::format("leaderboard_{}", event->leaderboard->id),
static_cast<float>(g_settings.achievements_leaderboard_duration), s_state.game_icon,
std::string(event->leaderboard->title), std::move(message), ICON_EMOJI_CHECKMARK_BUTTON,
FullscreenUI::AchievementNotificationNoteType::IconText, LEADERBOARD_NOTIFICATION_MIN_WIDTH);
}
}
@@ -1584,11 +1589,11 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_
// we still track these even if the option is disabled, so that they can be displayed in the pause menu
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification)
{
FullscreenUI::AddAchievementNotification(fmt::format("AchievementChallenge{}", event->achievement->id),
CHALLENGE_STARTED_NOTIFICATION_TIME, badge_path,
fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"),
event->achievement->title ? event->achievement->title : ""),
event->achievement->description, {});
FullscreenUI::AddAchievementNotification(
fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_STARTED_NOTIFICATION_TIME, badge_path,
fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"),
event->achievement->title ? event->achievement->title : ""),
fmt::format(ICON_EMOJI_DIRECT_HIT " {}", event->achievement->description ? event->achievement->description : ""));
}
s_state.active_challenge_indicators.push_back(
@@ -1613,11 +1618,12 @@ void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification &&
event->achievement->state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
{
FullscreenUI::AddAchievementNotification(fmt::format("AchievementChallenge{}", event->achievement->id),
CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_path,
fmt::format(TRANSLATE_FS("Achievements", "Challenge Failed: {}"),
event->achievement->title ? event->achievement->title : ""),
event->achievement->description, {});
FullscreenUI::AddAchievementNotification(
fmt::format("AchievementChallenge{}", event->achievement->id), CHALLENGE_FAILED_NOTIFICATION_TIME, it->badge_path,
fmt::format(TRANSLATE_FS("Achievements", "Challenge Failed: {}"),
event->achievement->title ? event->achievement->title : ""),
fmt::format(ICON_EMOJI_CROSS_MARK_BUTTON " {}",
event->achievement->description ? event->achievement->description : ""));
}
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification ||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Disabled)
@@ -2079,8 +2085,7 @@ void Achievements::ClientLoginWithTokenCallback(int result, const char* error_me
TRANSLATE_STR("Achievements", "RetroAchievements Login Failed"),
fmt::format(
TRANSLATE_FS("Achievements", "Achievement unlocks will not be submitted for this session.\nError: {}"),
error_message),
{});
error_message));
}
return;
@@ -2120,7 +2125,8 @@ void Achievements::FinishLogin()
user->score, user->score_softcore, user->num_unread_messages);
FullscreenUI::AddAchievementNotification("achievements_login", LOGIN_NOTIFICATION_TIME, s_state.user_badge_path,
user->display_name, std::move(summary), {});
user->display_name, std::move(summary), RA_LOGO_ICON_NAME,
FullscreenUI::AchievementNotificationNoteType::Image);
}
}

View File

@@ -16,7 +16,6 @@ namespace Achievements {
inline constexpr float INDICATOR_FADE_IN_TIME = 0.2f;
inline constexpr float INDICATOR_FADE_OUT_TIME = 0.4f;
extern const std::string_view NOTIFICATION_SPINNER_NOTE;
struct LeaderboardTrackerIndicator
{

View File

@@ -7,6 +7,7 @@
#include "host.h"
#include "system.h"
#include "util/gpu_texture.h"
#include "util/imgui_manager.h"
#include "util/translation.h"
@@ -59,7 +60,8 @@ struct Notification
float duration;
float target_y;
float last_y;
float min_width;
u16 min_width;
AchievementNotificationNoteType note_type;
};
struct PauseMenuAchievementInfo
@@ -238,7 +240,8 @@ void FullscreenUI::DrawAchievementsOverlays()
}
void FullscreenUI::AddAchievementNotification(std::string key, float duration, std::string image_path,
std::string title, std::string text, std::string note, float min_width)
std::string title, std::string text, std::string note,
AchievementNotificationNoteType note_type, u16 min_width)
{
const bool prev_had_notifications = s_achievements_locals.notifications.empty();
const Timer::Value current_time = Timer::GetCurrentValue();
@@ -255,6 +258,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s
it->note = std::move(note);
it->badge_path = std::move(image_path);
it->min_width = min_width;
it->note_type = note_type;
// Don't fade it in again
const float time_passed = static_cast<float>(Timer::ConvertValueToSeconds(current_time - it->start_time));
@@ -277,6 +281,7 @@ void FullscreenUI::AddAchievementNotification(std::string key, float duration, s
notif.target_y = -1.0f;
notif.last_y = -1.0f;
notif.min_width = min_width;
notif.note_type = note_type;
s_achievements_locals.notifications.push_back(std::move(notif));
if (!prev_had_notifications)
@@ -302,8 +307,8 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout)
const float max_text_width = max_width - badge_size - (horizontal_padding * 2.0f) - horizontal_spacing;
const float min_height = (vertical_padding * 2.0f) + badge_size;
const float rounding = FullscreenUI::LayoutScale(20.0f);
const float spinner_size = FullscreenUI::LayoutScale(16.0f);
const float min_rounded_width = rounding * 2.0f;
const float note_icon_padding = LayoutScale(30.0f);
ImFont*& font = UIStyle.Font;
const float& title_font_size = UIStyle.LargeFontSize;
@@ -329,26 +334,67 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout)
}
// place note to the right of the title
const bool is_spinner = (notif.note == Achievements::NOTIFICATION_SPINNER_NOTE);
const bool is_icon_note =
(!is_spinner && !notif.note.empty() && (StringUtil::GetUTF8CharacterCount(notif.note) == 1));
const float note_font_size = is_spinner ? spinner_size : (is_icon_note ? note_icon_size : note_text_size);
const float note_font_weight = is_icon_note ? UIStyle.NormalFontWeight : note_text_weight;
const ImVec2 note_size = is_spinner ?
LayoutScale(note_font_size, note_font_size) :
(notif.note.empty() ? ImVec2() :
font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX,
0.0f, IMSTR_START_END(notif.note)));
GPUTexture* note_image = nullptr;
float note_font_size, note_font_weight, note_offset_y, note_spacing;
ImVec2 note_size;
switch (notif.note_type)
{
case AchievementNotificationNoteType::Text:
note_font_size = note_text_size;
note_font_weight = note_text_weight;
note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note));
note_offset_y = 0.0f;
note_spacing = larger_horizontal_spacing;
break;
case AchievementNotificationNoteType::IconText:
note_font_size = note_icon_size;
note_font_weight = UIStyle.NormalFontWeight;
note_size = font->CalcTextSizeA(note_font_size, note_font_weight, FLT_MAX, 0.0f, IMSTR_START_END(notif.note));
note_offset_y = 0.0f;
note_spacing = note_icon_padding;
break;
case AchievementNotificationNoteType::Spinner:
note_font_size = 0.0f;
note_font_weight = 0.0f;
note_size = ImVec2(note_text_size, note_text_size);
note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f);
note_spacing = note_icon_padding;
break;
case AchievementNotificationNoteType::Image:
note_font_size = 0.0f;
note_font_weight = 0.0f;
note_image = GetCachedTexture(notif.note, static_cast<u32>(note_text_size), static_cast<u32>(note_text_size));
note_size = (note_image && note_image->GetWidth() > note_image->GetHeight()) ?
ImVec2(note_text_size * (static_cast<float>(note_image->GetWidth()) /
static_cast<float>(note_image->GetHeight())),
note_text_size) :
ImVec2(note_text_size, note_text_size * (static_cast<float>(note_image->GetHeight()) /
static_cast<float>(note_image->GetWidth())));
note_offset_y = ImFloor((note_icon_size - note_text_size) * 0.5f);
note_spacing = note_icon_padding;
break;
case AchievementNotificationNoteType::None:
default:
note_font_size = 0.0f;
note_font_weight = 0.0f;
note_size = ImVec2();
note_offset_y = 0.0f;
note_spacing = 0.0f;
break;
}
const ImVec2 title_size = font->CalcTextSizeA(title_font_size, title_font_weight, max_text_width - note_size.x,
max_text_width - note_size.x, IMSTR_START_END(notif.title));
const ImVec2 text_size = font->CalcTextSizeA(text_font_size, text_font_weight, max_text_width, max_text_width,
IMSTR_START_END(notif.text));
const float box_width =
std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing +
ImCeil(std::max(title_size.x + (notif.note.empty() ? 0.0f : (larger_horizontal_spacing + note_size.x)),
text_size.x)),
std::max(LayoutScale(notif.min_width), min_width));
const float box_width = std::max((horizontal_padding * 2.0f) + badge_size + horizontal_spacing +
ImCeil(std::max(title_size.x + note_spacing + note_size.x, text_size.x)),
std::max(static_cast<float>(LayoutScale(notif.min_width)), min_width));
const float box_height =
std::max((vertical_padding * 2.0f) + ImCeil(title_size.y) + vertical_spacing + ImCeil(text_size.y), min_height);
@@ -424,18 +470,38 @@ void FullscreenUI::DrawNotifications(NotificationLayout& layout)
RenderShadowedTextClipped(dl, font, text_font_size, text_font_weight, text_bb.Min, text_bb.Max, text_col,
notif.text, &text_size, ImVec2(0.0f, 0.0f), max_text_width, &text_bb);
if (notif.note == Achievements::NOTIFICATION_SPINNER_NOTE)
const ImVec2 note_pos =
ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding + note_offset_y);
switch (notif.note_type)
{
DrawSpinner(dl, ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding),
title_col, note_size.x, LayoutScale(4.0f));
}
else if (!notif.note.empty())
{
const ImVec2 note_pos =
ImVec2((box_min.x + box_width) - horizontal_padding - note_size.x, box_min.y + vertical_padding);
const ImRect note_bb = ImRect(note_pos, note_pos + note_size);
RenderShadowedTextClipped(dl, font, note_font_size, note_font_weight, note_bb.Min, note_bb.Max, title_col,
notif.note, &note_size, ImVec2(0.0f, 0.0f), max_text_width, &note_bb);
case AchievementNotificationNoteType::Text:
case AchievementNotificationNoteType::IconText:
{
const ImRect note_bb = ImRect(note_pos, note_pos + note_size);
RenderShadowedTextClipped(dl, font, note_font_size, note_font_weight, note_bb.Min, note_bb.Max, title_col,
notif.note, &note_size, ImVec2(0.0f, 0.0f), max_text_width, &note_bb);
}
break;
case AchievementNotificationNoteType::Spinner:
{
DrawSpinner(dl, note_pos, title_col, note_size.x, LayoutScale(4.0f));
}
break;
case AchievementNotificationNoteType::Image:
{
if (note_image)
{
const ImRect image_rect = CenterImage(note_size, note_image);
dl->AddImage(note_image, note_pos + image_rect.Min, note_pos + image_rect.Max);
}
}
break;
case AchievementNotificationNoteType::None:
default:
break;
}
++iter;

View File

@@ -119,9 +119,20 @@ bool IsInputBindingDialogOpen();
// Achievements
//////////////////////////////////////////////////////////////////////////
enum class AchievementNotificationNoteType
{
None,
Text,
IconText,
Spinner,
Image,
};
/// Schedules an achievement notification to be shown.
void AddAchievementNotification(std::string key, float duration, std::string image_path, std::string title,
std::string text, std::string note = {}, float min_width = 0.0f);
std::string text, std::string note = {},
AchievementNotificationNoteType note_type = AchievementNotificationNoteType::None,
u16 min_width = 0);
/// Draws ImGui overlays when ingame.
void DrawAchievementsOverlays();