diff --git a/src/core/settings.cpp b/src/core/settings.cpp index e0b754480..e166558eb 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -347,6 +347,7 @@ void Settings::Load(SettingsInterface& si) achievements_challenge_mode = si.GetBoolValue("Cheevos", "ChallengeMode", false); achievements_leaderboards = si.GetBoolValue("Cheevos", "Leaderboards", true); achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true); + achievements_primed_indicators = si.GetBoolValue("Cheevos", "PrimedIndicators", true); log_level = ParseLogLevelName(si.GetStringValue("Logging", "LogLevel", GetLogLevelName(DEFAULT_LOG_LEVEL)).c_str()) .value_or(DEFAULT_LOG_LEVEL); @@ -534,6 +535,7 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Cheevos", "ChallengeMode", achievements_challenge_mode); si.SetBoolValue("Cheevos", "Leaderboards", achievements_leaderboards); si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects); + si.SetBoolValue("Cheevos", "PrimedIndicators", achievements_primed_indicators); si.SetStringValue("Logging", "LogLevel", GetLogLevelName(log_level)); si.SetStringValue("Logging", "LogFilter", log_filter.c_str()); diff --git a/src/core/settings.h b/src/core/settings.h index 903c6450f..c2b976bd2 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -178,6 +178,7 @@ struct Settings bool achievements_challenge_mode : 1; bool achievements_leaderboards : 1; bool achievements_sound_effects : 1; + bool achievements_primed_indicators : 1; #endif struct DebugSettings diff --git a/src/duckstation-qt/achievementsettingswidget.cpp b/src/duckstation-qt/achievementsettingswidget.cpp index 5691625dd..6f9def962 100644 --- a/src/duckstation-qt/achievementsettingswidget.cpp +++ b/src/duckstation-qt/achievementsettingswidget.cpp @@ -26,6 +26,7 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsDialog* dialog, QWi "UseFirstDiscFromPlaylist", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboards, "Cheevos", "Leaderboards", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.primedIndicators, "Cheevos", "PrimedIndicators", true); dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"), tr("When enabled and logged in, DuckStation will scan for achievements on startup.")); @@ -53,6 +54,9 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsDialog* dialog, QWi m_ui.leaderboards, tr("Enable Leaderboards"), tr("Checked"), tr("Enables tracking and submission of leaderboards in supported games. If leaderboards " "are disabled, you will still be able to view the leaderboard and scores, but no scores will be uploaded.")); + dialog->registerWidgetHelp( + m_ui.primedIndicators, tr("Show Challenge Indicators"), tr("Checked"), + tr("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.")); connect(m_ui.enable, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); connect(m_ui.challengeMode, &QCheckBox::stateChanged, this, &AchievementSettingsWidget::updateEnableState); @@ -95,6 +99,7 @@ void AchievementSettingsWidget::updateEnableState() m_ui.leaderboards->setEnabled(enabled && challenge); m_ui.unofficialTestMode->setEnabled(enabled); m_ui.soundEffects->setEnabled(enabled); + m_ui.primedIndicators->setEnabled(enabled); } void AchievementSettingsWidget::onChallengeModeStateChanged() diff --git a/src/duckstation-qt/achievementsettingswidget.ui b/src/duckstation-qt/achievementsettingswidget.ui index 972d67f2a..b5995a6e9 100644 --- a/src/duckstation-qt/achievementsettingswidget.ui +++ b/src/duckstation-qt/achievementsettingswidget.ui @@ -7,7 +7,7 @@ 0 0 648 - 456 + 475 @@ -32,13 +32,6 @@ Global Settings - - - - Enable Achievements - - - @@ -46,27 +39,6 @@ - - - - Enable Rich Presence - - - - - - - Enable Test Mode - - - - - - - Enable Hardcore Mode - - - @@ -74,10 +46,10 @@ - - + + - Use First Disc From Playlist + Enable Achievements @@ -88,6 +60,41 @@ + + + + Show Challenge Indicators + + + + + + + Enable Rich Presence + + + + + + + Enable Hardcore Mode + + + + + + + Use First Disc From Playlist + + + + + + + Enable Test Mode + + + @@ -130,7 +137,7 @@ 0 - 160 + 120 diff --git a/src/frontend-common/achievements.cpp b/src/frontend-common/achievements.cpp index 4aa3f5696..d2e7bcb90 100644 --- a/src/frontend-common/achievements.cpp +++ b/src/frontend-common/achievements.cpp @@ -867,7 +867,7 @@ void Achievements::LoginCallback(s32 status_code, std::string content_type, Comm RAPIResponse response( status_code, data); - if (!response) + if (!response || !response.username || !response.api_token) { FormattedError("Login failed. Please check your user name and password, and try again."); return; @@ -1104,7 +1104,7 @@ void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, std::unique_lock lock(s_achievements_mutex); ClearGameInfo(); - if (!response) + if (!response || !response.title) { DisableChallengeMode(); return; @@ -1117,7 +1117,7 @@ void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, s_game_title = response.title; // try for a icon - if (std::strlen(response.image_name) > 0) + if (response.image_name && std::strlen(response.image_name) > 0) { s_game_icon = Path::Combine(s_game_icon_cache_directory, fmt::format("{}.png", s_game_id)); if (!FileSystem::FileExists(s_game_icon.c_str())) @@ -1155,6 +1155,12 @@ void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, continue; } + if (!defn.definition || !defn.title || !defn.description || !defn.badge_name) + { + Log_ErrorPrintf("Incomplete achievement %u", defn.id); + continue; + } + Achievement cheevo; cheevo.id = defn.id; cheevo.memaddr = defn.definition; @@ -1172,6 +1178,11 @@ void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, for (u32 i = 0; i < response.num_leaderboards; i++) { const rc_api_leaderboard_definition_t& defn = response.leaderboards[i]; + if (!defn.title || !defn.description || !defn.definition) + { + Log_ErrorPrintf("Incomplete achievement %u", defn.id); + continue; + } Leaderboard lboard; lboard.id = defn.id; @@ -1192,7 +1203,7 @@ void Achievements::GetPatchesCallback(s32 status_code, std::string content_type, } // parse rich presence - if (std::strlen(response.rich_presence_script) > 0) + if (response.rich_presence_script && std::strlen(response.rich_presence_script) > 0) { const int res = rc_runtime_activate_richpresence(&s_rcheevos_runtime, response.rich_presence_script, nullptr, 0); if (res == RC_OK) @@ -1263,6 +1274,8 @@ void Achievements::GetLbInfoCallback(s32 status_code, std::string content_type, for (u32 i = 0; i < response.num_entries; i++) { const rc_api_lboard_info_entry_t& entry = response.entries[i]; + if (!entry.username) + continue; char score[128]; rc_runtime_format_lboard_value(score, sizeof(score), entry.score, leaderboard->format); @@ -1903,16 +1916,18 @@ TinyString Achievements::GetAchievementProgressText(const Achievement& achieveme return buf; } -const std::string& Achievements::GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing) +const std::string& Achievements::GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing, + bool force_unlocked_icon) { - std::string& badge_path = achievement.locked ? achievement.locked_badge_path : achievement.unlocked_badge_path; + const bool use_locked = (achievement.locked && !force_unlocked_icon); + std::string& badge_path = use_locked ? achievement.locked_badge_path : achievement.unlocked_badge_path; if (!badge_path.empty() || achievement.badge_name.empty()) return badge_path; // well, this comes from the internet.... :) const std::string clean_name(Path::SanitizeFileName(achievement.badge_name)); - badge_path = Path::Combine(s_achievement_icon_cache_directory, - fmt::format("{}{}.png", clean_name, achievement.locked ? "_lock" : "")); + badge_path = + Path::Combine(s_achievement_icon_cache_directory, fmt::format("{}{}.png", clean_name, use_locked ? "_lock" : "")); if (FileSystem::FileExists(badge_path.c_str())) return badge_path; @@ -1921,7 +1936,7 @@ const std::string& Achievements::GetAchievementBadgePath(const Achievement& achi { RAPIRequest request; request.image_name = achievement.badge_name.c_str(); - request.image_type = achievement.locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; + request.image_type = use_locked ? RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED : RC_IMAGE_TYPE_ACHIEVEMENT; request.DownloadImage(badge_path); } diff --git a/src/frontend-common/achievements.h b/src/frontend-common/achievements.h index b3e87025e..1aeab25cb 100644 --- a/src/frontend-common/achievements.h +++ b/src/frontend-common/achievements.h @@ -149,7 +149,8 @@ u32 GetPrimedAchievementCount(); const Achievement* GetAchievementByID(u32 id); std::pair GetAchievementProgress(const Achievement& achievement); TinyString GetAchievementProgressText(const Achievement& achievement); -const std::string& GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing = true); +const std::string& GetAchievementBadgePath(const Achievement& achievement, bool download_if_missing = true, + bool force_unlocked_icon = false); std::string GetAchievementBadgeURL(const Achievement& achievement); #ifdef WITH_RAINTEGRATION diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index c7da2b1e8..08007f594 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -427,7 +427,8 @@ static GameListPage s_game_list_page = GameListPage::Grid; ////////////////////////////////////////////////////////////////////////// static void DrawAchievementsWindow(); static void DrawAchievement(const Achievements::Achievement& cheevo); -static void DrawPrimedAchievements(); +static void DrawPrimedAchievementsIcons(); +static void DrawPrimedAchievementsList(); static void DrawLeaderboardsWindow(); static void DrawLeaderboardListEntry(const Achievements::Leaderboard& lboard); static void DrawLeaderboardEntry(const Achievements::LeaderboardEntry& lbEntry, float rank_column_width, @@ -723,8 +724,11 @@ void FullscreenUI::Render() #ifdef WITH_CHEEVOS // Primed achievements must come first, because we don't want the pause screen to be behind them. - if (Achievements::GetPrimedAchievementCount() > 0) - DrawPrimedAchievements(); + if (g_settings.achievements_primed_indicators && s_current_main_window == MainWindowType::None && + Achievements::GetPrimedAchievementCount() > 0) + { + DrawPrimedAchievementsIcons(); + } #endif switch (s_current_main_window) @@ -3579,6 +3583,10 @@ void FullscreenUI::DrawAchievementsSettingsPage() DrawToggleSetting(bsi, ICON_FA_HEADPHONES " Sound Effects", "Plays sound effects for events such as achievement unlocks and leaderboard submissions.", "Cheevos", "SoundEffects", true, enabled); + DrawToggleSetting( + bsi, ICON_FA_MAGIC " Show Challenge Indicators", + "Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.", "Cheevos", + "PrimedIndicators", true, enabled); DrawToggleSetting(bsi, ICON_FA_MEDAL " Test Unofficial Achievements", "When enabled, DuckStation will list achievements from unofficial sets. These achievements are not " "tracked by RetroAchievements.", @@ -4036,6 +4044,11 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) EndFullscreenWindow(); } + +#ifdef WITH_CHEEVOS + if (Achievements::GetPrimedAchievementCount() > 0) + DrawPrimedAchievementsList(); +#endif } void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, @@ -5572,7 +5585,7 @@ void FullscreenUI::DrawAchievementsWindow() EndFullscreenWindow(); } -void FullscreenUI::DrawPrimedAchievements() +void FullscreenUI::DrawPrimedAchievementsIcons() { const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); const float spacing = LayoutScale(10.0f); @@ -5588,7 +5601,7 @@ void FullscreenUI::DrawPrimedAchievements() if (!achievement.primed) return true; - const std::string& badge_path = Achievements::GetAchievementBadgePath(achievement); + const std::string& badge_path = Achievements::GetAchievementBadgePath(achievement, true, true); if (badge_path.empty()) return true; @@ -5603,6 +5616,80 @@ void FullscreenUI::DrawPrimedAchievements() }); } +void FullscreenUI::DrawPrimedAchievementsList() +{ + auto lock = Achievements::GetLock(); + const u32 primed_count = Achievements::GetPrimedAchievementCount(); + + const ImGuiIO& io = ImGui::GetIO(); + ImFont* font = g_medium_font; + + const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)); + const float margin = LayoutScale(10.0f); + const float spacing = LayoutScale(10.0f); + const float padding = LayoutScale(10.0f); + + const float max_text_width = LayoutScale(300.0f); + const float row_width = max_text_width + padding + padding + image_size.x + spacing; + const float title_height = padding + font->FontSize + padding; + const ImVec2 box_min(io.DisplaySize.x - row_width - margin, margin); + const ImVec2 box_max(box_min.x + row_width, + box_min.y + title_height + (static_cast(primed_count) * (image_size.y + padding))); + + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f)); + dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255), + "Active Challenge Achievements"); + + const float y_advance = image_size.y + spacing; + const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f; + const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f); + ImVec2 position(box_min.x + padding, box_min.y + title_height); + + Achievements::EnumerateAchievements([font, &image_size, max_text_width, spacing, y_advance, acheivement_name_offset, + max_non_ellipised_text_width, + &position](const Achievements::Achievement& achievement) { + if (!achievement.primed) + return true; + + const std::string& badge_path = Achievements::GetAchievementBadgePath(achievement, true, true); + if (badge_path.empty()) + return true; + + GPUTexture* badge = GetCachedTextureAsync(badge_path.c_str()); + if (!badge) + return true; + + ImDrawList* dl = ImGui::GetBackgroundDrawList(); + dl->AddImage(badge, position, position + image_size); + + const char* achievement_title = achievement.title.c_str(); + const char* achievement_tile_end = achievement_title + achievement.title.length(); + const char* remaining_text = nullptr; + const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title, + achievement_tile_end, &remaining_text)); + const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset); + const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width, + text_position.y + image_size.y); + const u32 text_color = IM_COL32(255, 255, 255, 255); + + if (remaining_text < achievement_tile_end) + { + dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f, &text_bbox); + dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...", + nullptr, 0.0f, &text_bbox); + } + else + { + dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, + achievement_title + achievement.title.length(), 0.0f, &text_bbox); + } + + position.y += y_advance; + return true; + }); +} + bool FullscreenUI::OpenLeaderboardsWindow() { if (!System::IsValid() || !Achievements::HasActiveGame() || Achievements::GetLeaderboardCount() == 0 || !Initialize()) diff --git a/src/frontend-common/imgui_fullscreen.cpp b/src/frontend-common/imgui_fullscreen.cpp index 08fdafb14..2cc9e5c5e 100644 --- a/src/frontend-common/imgui_fullscreen.cpp +++ b/src/frontend-common/imgui_fullscreen.cpp @@ -2145,7 +2145,7 @@ void ImGuiFullscreen::DrawMessageDialog() } static float s_notification_vertical_position = 0.3f; -static float s_notification_vertical_direction = -1.0f; +static float s_notification_vertical_direction = 1.0f; float ImGuiFullscreen::GetNotificationVerticalPosition() {