From c73ef15ebf64a495dde528175fd454c3d1472623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Jun 2025 18:24:43 +0900 Subject: [PATCH] Ensure valid ruleset for gameplay --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 14 +++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +- .../SelectV2/BeatmapCarouselFilterMatching.cs | 4 +- osu.Game/Screens/SelectV2/SongSelect.cs | 92 +++++++++++-------- 4 files changed, 74 insertions(+), 44 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index 25f98c812c..d25a171023 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -64,6 +64,20 @@ namespace osu.Game.Beatmaps private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + /// + /// Whether gameplay is allowed for this beatmap with the provided ruleset (via conversion or direct compatibility). + /// + public static bool AllowGameplayWithRuleset(this IBeatmapInfo beatmap, RulesetInfo ruleset, bool allowConversion) + { + if (beatmap.Ruleset.ShortName == ruleset.ShortName) + return true; + + if (allowConversion && beatmap.Ruleset.OnlineID == 0 && ruleset.OnlineID != 0) + return true; + + return false; + } + /// /// Get the beatmap info page URL, or null if unavailable. /// diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 19333a97b5..cc40921562 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Threading; using osu.Framework.Utils; @@ -495,7 +496,7 @@ namespace osu.Game.Screens.SelectV2 private ScheduledDelegate? loadingDebounce; - public void Filter(FilterCriteria criteria) + public void Filter(FilterCriteria criteria, bool showLoadingImmediately = false) { bool resetDisplay = grouping.BeatmapSetsGroupedTogether != BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria); @@ -503,9 +504,12 @@ namespace osu.Game.Screens.SelectV2 loadingDebounce ??= Scheduler.AddDelayed(() => { + if (loading.State.Value == Visibility.Visible) + return; + Scroll.FadeColour(OsuColour.Gray(0.5f), 1000, Easing.OutQuint); loading.Show(); - }, 250); + }, showLoadingImmediately ? 0 : 250); FilterAsync(resetDisplay).ContinueWith(_ => Schedule(() => { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index 545fbbd5fd..a776b2f796 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -56,9 +56,7 @@ namespace osu.Game.Screens.SelectV2 private static bool checkCriteriaMatch(BeatmapInfo beatmap, FilterCriteria criteria) { - bool match = criteria.Ruleset == null || - beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName || - (beatmap.Ruleset.OnlineID == 0 && criteria.Ruleset.OnlineID != 0 && criteria.AllowConvertedBeatmaps); + bool match = criteria.Ruleset == null || beatmap.AllowGameplayWithRuleset(criteria.Ruleset!, criteria.AllowConvertedBeatmaps); if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index a732f1447b..8c362c2b44 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.SelectV2 .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); }); - Beatmap.BindValueChanged(_ => EnsureValidSelection()); + Beatmap.BindValueChanged(_ => ensureGlobalBeatmapValid()); } protected override void Update() @@ -406,18 +406,18 @@ namespace osu.Game.Screens.SelectV2 if (!this.IsCurrentScreen()) return; + // `ensureGlobalBeatmapValid` also performs this checks, but it will change the active selection on fail. + // By checking locally first, we can correctly perform a no-op rather than changing selection. + if (!checkBeatmapValidForSelection(beatmap, carousel.Criteria)) + return; + // Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific). Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true); if (Beatmap.IsDefault) return; - // EnsureValidSelection also performs these checks, but it will change the active selection on fail. - // We want no-op for such an edge case, so early return. - if (beatmap.BeatmapSet!.Protected || beatmap.BeatmapSet!.DeletePending) - return; - - if (!EnsureValidSelection()) + if (!ensureGlobalBeatmapValid()) return; startAction(); @@ -440,33 +440,57 @@ namespace osu.Game.Screens.SelectV2 selectionDebounce = Scheduler.AddDelayed(() => Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap), SELECTION_DEBOUNCE); } - protected bool EnsureValidSelection() + private bool ensureGlobalBeatmapValid() { if (!this.IsCurrentScreen()) return false; - bool validSelection = true; + // While filtering, let's not ever attempt to change selection. + // This will be resolved after the filter completes, see `newItemsPresented`. + bool carouselStateIsValid = filterDebounce?.State != ScheduledDelegate.RunState.Waiting && !carousel.IsFiltering; + if (!carouselStateIsValid) + return false; - if (Beatmap.Value.BeatmapSetInfo.Protected || Beatmap.Value.BeatmapSetInfo.DeletePending) + // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. + var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); + bool validSelection = checkBeatmapValidForSelection(currentBeatmap.BeatmapInfo, carousel.Criteria); + + if (Beatmap.IsDefault || !validSelection) { - if (!carousel.NextRandom()) - { - Beatmap.SetDefault(); - validSelection = false; - } + validSelection = carousel.NextRandom(); + if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting) + selectionDebounce?.RunTask(); } - carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; + if (validSelection) + carousel.CurrentSelection = Beatmap.Value.BeatmapInfo; + else + Beatmap.SetDefault(); ensurePlayingSelected(); updateBackgroundDim(); - if (!validSelection) + return validSelection; + } + + private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria) + { + if (criteria == null) return false; - // TODO: Add things here like ruleset validation. Or maybe a forced carousel filter. + if (!beatmap.AllowGameplayWithRuleset(Ruleset.Value, criteria.AllowConvertedBeatmaps)) + return false; - return validSelection; + if (beatmap.Hidden) + return false; + + if (beatmap.BeatmapSet == null) + return false; + + if (beatmap.BeatmapSet.Protected || beatmap.BeatmapSet.DeletePending) + return false; + + return true; } #endregion @@ -523,7 +547,7 @@ namespace osu.Game.Screens.SelectV2 beginLooping(); attachTrackDuckingIfShould(); - EnsureValidSelection(); + ensureGlobalBeatmapValid(); } private void onLeavingScreen() @@ -600,11 +624,15 @@ namespace osu.Game.Screens.SelectV2 private void criteriaChanged(FilterCriteria criteria) { - // The first filter needs to be applied immediately as this triggers the initial carousel load. - double filterDelay = filterDebounce == null ? 0 : filter_delay; - filterDebounce?.Cancel(); - filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filterDelay); + + // The first filter needs to be applied immediately as this triggers the initial carousel load. + bool isFirstFilter = filterDebounce == null; + + // Criteria change may have included a ruleset change which made the current selection invalid. + bool isSelectionValid = checkBeatmapValidForSelection(Beatmap.Value.BeatmapInfo, criteria); + + filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria, !isSelectionValid); }, isFirstFilter || !isSelectionValid ? 0 : filter_delay); } private void newItemsPresented(IEnumerable carouselItems) @@ -620,21 +648,7 @@ namespace osu.Game.Screens.SelectV2 // but also in this case we want support for formatting a number within a string). filterControl.StatusText = count != 1 ? $"{count:#,0} matches" : $"{count:#,0} match"; - // Refetch to be confident that the current selection is still valid. It may have been deleted or hidden. - var currentBeatmap = beatmaps.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo, true); - bool currentBeatmapNotValid = currentBeatmap.BeatmapInfo.Hidden || currentBeatmap.BeatmapSetInfo?.DeletePending == true; - - // If all results are filtered away don't deselect the current global beatmap selection... - if (!carouselItems.Any()) - { - // ...unless it has been deleted or hidden - if (currentBeatmapNotValid) - Beatmap.SetDefault(); - return; - } - - if (Beatmap.IsDefault || currentBeatmapNotValid) - carousel.NextRandom(); + ensureGlobalBeatmapValid(); } private void updateNoResultsPlaceholder()