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()