From 25a3ac2c00be97f85d0fbb58d9a1ab8a082d14ce Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 18 May 2025 00:56:35 +0300 Subject: [PATCH 01/17] Tidy up `GroupMode` enum --- .../Visual/SongSelectV2/SongSelectTestScene.cs | 2 +- .../SongSelectV2/TestSceneBeatmapCarousel.cs | 2 +- .../TestSceneBeatmapCarouselNoGrouping.cs | 2 +- .../Visual/UserInterface/TestSceneTabControl.cs | 4 ++-- osu.Game/Configuration/OsuConfigManager.cs | 6 +++++- osu.Game/Screens/Select/Filter/GroupMode.cs | 17 +++++++++-------- osu.Game/Screens/SelectV2/FilterControl.cs | 4 +++- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index 4ca6c5a549..ce5fa228c9 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectedMods.SetDefault(); Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title); - Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.All); + Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.NoGrouping); SongSelect = null!; }); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 21030e0b88..3e3c7504dd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Explicit] public void TestSorting() { - SortAndGroupBy(SortMode.Artist, GroupMode.All); + SortAndGroupBy(SortMode.Artist, GroupMode.NoGrouping); SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty); SortAndGroupBy(SortMode.Artist, GroupMode.Artist); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 3ca8773adb..904967c1f3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddBeatmaps(2, 3); WaitForDrawablePanels(); - SortAndGroupBy(SortMode.Difficulty, GroupMode.All); + SortAndGroupBy(SortMode.Difficulty, GroupMode.NoGrouping); WaitForFiltering(); AddUntilStep("standalone panels displayed", () => GetVisiblePanels().Any()); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs index 94117ff7e3..bf75d07c2c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneTabControl.cs @@ -30,8 +30,8 @@ namespace osu.Game.Tests.Visual.UserInterface Position = new Vector2(275, 5) }); - filter.PinItem(GroupMode.All); - filter.PinItem(GroupMode.RecentlyPlayed); + filter.PinItem(GroupMode.NoGrouping); + filter.PinItem(GroupMode.LastPlayed); filter.Current.ValueChanged += grouping => { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 167e52ad0d..18d8f69918 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -47,7 +47,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1); - SetDefault(OsuSetting.SongSelectGroupingMode, GroupMode.All); + SetDefault(OsuSetting.SongSelectGroupingMode, GroupMode.NoGrouping); SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title); SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); @@ -263,6 +263,10 @@ namespace osu.Game.Configuration if (RuntimeInfo.IsMobile) GetBindable(OsuSetting.UIScale).SetDefault(); } + + if (combined < 20250518) + // GroupMode.All, the previous default grouping mode, is made obsolete and to be removed in favour of GroupMode.NoGrouping. + GetBindable(OsuSetting.SongSelectGroupingMode).SetDefault(); } public override TrackedSettings CreateTrackedSettings() diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index d794c215a3..a560c155ae 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -1,14 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; namespace osu.Game.Screens.Select.Filter { public enum GroupMode { - [Description("All")] - All, + [Description("No Grouping")] + NoGrouping, [Description("Artist")] Artist, @@ -37,19 +38,19 @@ namespace osu.Game.Screens.Select.Filter [Description("My Maps")] MyMaps, - [Description("No Grouping")] - NoGrouping, - [Description("Rank Achieved")] RankAchieved, [Description("Ranked Status")] RankedStatus, - [Description("Recently Played")] - RecentlyPlayed, + [Description("Last Played")] + LastPlayed, [Description("Title")] - Title + Title, + + [Obsolete($"Use {nameof(NoGrouping)} instead.")] // todo: remove in 20251018 + All, } } diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 5845c36882..036e5c85ca 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -162,7 +162,9 @@ namespace osu.Game.Screens.SelectV2 groupDropdown = new ShearedDropdown("Group by") { RelativeSizeAxes = Axes.X, - Items = Enum.GetValues(), +#pragma warning disable CS0618 // Type or member is obsolete + Items = Enum.GetValues().Where(m => m != GroupMode.All), +#pragma warning restore CS0618 // Type or member is obsolete }, Empty(), collectionDropdown = new CollectionDropdown From 82923e67697c43c3a97d867ee767ded8c8b0b82c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 18 May 2025 00:30:13 +0300 Subject: [PATCH 02/17] Add grouping support for most modes --- .../SongSelectV2/TestScenePanelGroup.cs | 8 +- osu.Game/Beatmaps/BeatmapOnlineStatus.cs | 1 + osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 8 +- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 297 ++++++++++++++---- .../SelectV2/PanelGroupStarDifficulty.cs | 2 +- 5 files changed, 247 insertions(+), 69 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index 2d1b7cd1b2..d91e7283d1 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -56,21 +56,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")) + Item = new CarouselItem(new GroupDefinition(0, $"{star} Star(s)", new StarDifficulty(star, 0))) }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")), + Item = new CarouselItem(new GroupDefinition(1, $"{star} Star(s)", new StarDifficulty(star, 0))), KeyboardSelected = { Value = true }, }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")), + Item = new CarouselItem(new GroupDefinition(2, $"{star} Star(s)", new StarDifficulty(star, 0))), Expanded = { Value = true }, }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(new StarDifficulty(star, 0), $"{star} Star(s)")), + Item = new CarouselItem(new GroupDefinition(3, $"{star} Star(s)", new StarDifficulty(star, 0))), Expanded = { Value = true }, KeyboardSelected = { Value = true }, }, diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index d489aeda3f..bc1438d7c7 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs @@ -15,6 +15,7 @@ namespace osu.Game.Beatmaps /// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted. /// [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))] + [Description("Local")] LocallyModified = -4, [Description("Unknown")] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4c70b8c58f..fa5224b387 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -423,5 +423,11 @@ namespace osu.Game.Screens.SelectV2 #endregion } - public record GroupDefinition(object Data, string Title); + /// + /// Defines a grouping header for a set of carousel items. + /// + /// The order of this group in the carousel, sorted using ascending order. + /// The title of this group. + /// Additional data. Provide a for difficulty groups, or null for any other group. + public record GroupDefinition(int Order, string Title, object? Data = null); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f8004282db..0286eadbcc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Graphics.Carousel; using osu.Game.Screens.Select; @@ -19,15 +21,15 @@ namespace osu.Game.Screens.SelectV2 /// /// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection. /// - public IDictionary> SetItems => setItems; + public IDictionary> SetItems => setMap; /// /// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection. /// - public IDictionary> GroupItems => groupItems; + public IDictionary> GroupItems => groupMap; - private readonly Dictionary> setItems = new Dictionary>(); - private readonly Dictionary> groupItems = new Dictionary>(); + private readonly Dictionary> setMap = new Dictionary>(); + private readonly Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; @@ -40,72 +42,70 @@ namespace osu.Game.Screens.SelectV2 { return await Task.Run(() => { - setItems.Clear(); - groupItems.Clear(); + setMap.Clear(); + groupMap.Clear(); var criteria = getCriteria(); var newItems = new List(); - BeatmapInfo? lastBeatmap = null; - - GroupDefinition? lastGroup = null; - CarouselItem? lastGroupItem = null; - - HashSet? currentGroupItems = null; - HashSet? currentSetItems = null; - BeatmapSetsGroupedTogether = criteria.Sort != SortMode.Difficulty; - foreach (var item in items) + var groups = getGroups((List)items, criteria); + + foreach (var (group, itemsInGroup) in groups) { cancellationToken.ThrowIfCancellationRequested(); - var beatmap = (BeatmapInfo)item.Model; + CarouselItem? groupItem = null; + HashSet? currentGroupItems = null; + HashSet? currentSetItems = null; + BeatmapInfo? lastBeatmap = null; - if (createGroupIfRequired(criteria, beatmap, lastGroup) is GroupDefinition newGroup) + if (group != null) { - // When reaching a new group, ensure we reset any beatmap set tracking. - currentSetItems = null; - lastBeatmap = null; + groupMap[group] = currentGroupItems = new HashSet(); - groupItems[newGroup] = currentGroupItems = new HashSet(); - lastGroup = newGroup; - - addItem(lastGroupItem = new CarouselItem(newGroup) + addItem(groupItem = new CarouselItem(group) { DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, }); } - if (BeatmapSetsGroupedTogether) + foreach (var item in itemsInGroup) { - bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + var beatmap = (BeatmapInfo)item.Model; - if (newBeatmapSet) + if (BeatmapSetsGroupedTogether) { - setItems[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; - if (lastGroupItem != null) - lastGroupItem.NestedItemCount++; - - addItem(new CarouselItem(beatmap.BeatmapSet!) + if (newBeatmapSet) { - DrawHeight = PanelBeatmapSet.HEIGHT, - DepthLayer = -1 - }); + if (!setMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) + setMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + + if (groupItem != null) + groupItem.NestedItemCount++; + + addItem(new CarouselItem(beatmap.BeatmapSet!) + { + DrawHeight = PanelBeatmapSet.HEIGHT, + DepthLayer = -1 + }); + } } - } - else - { - if (lastGroupItem != null) - lastGroupItem.NestedItemCount++; + else + { + if (groupItem != null) + groupItem.NestedItemCount++; - item.DrawHeight = PanelBeatmapStandalone.HEIGHT; - } + item.DrawHeight = PanelBeatmapStandalone.HEIGHT; + } - addItem(item); - lastBeatmap = beatmap; + addItem(item); + lastBeatmap = beatmap; + } void addItem(CarouselItem i) { @@ -114,7 +114,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (lastGroup == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || currentSetItems == null)); } } @@ -122,40 +122,211 @@ namespace osu.Game.Screens.SelectV2 }, cancellationToken).ConfigureAwait(false); } - private GroupDefinition? createGroupIfRequired(FilterCriteria criteria, BeatmapInfo beatmap, GroupDefinition? lastGroup) + private List getGroups(List items, FilterCriteria criteria) { switch (criteria.Group) { +#pragma warning disable CS0618 // Type or member is obsolete + case GroupMode.All: +#pragma warning restore CS0618 // Type or member is obsolete + case GroupMode.NoGrouping: + return new List { new GroupMapping(null, items) }; + case GroupMode.Artist: - char groupChar = lastGroup?.Data as char? ?? (char)0; - char beatmapFirstChar = char.ToUpperInvariant(beatmap.Metadata.Artist[0]); + return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Artist), items); - if (beatmapFirstChar > groupChar) - return new GroupDefinition(beatmapFirstChar, $"{beatmapFirstChar}"); + case GroupMode.Author: + return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Author.Username), items); - break; + case GroupMode.Title: + return getGroupsBy(b => defineGroupAlphabetically(b.BeatmapSet!.Metadata.Title), items); + + case GroupMode.DateAdded: + return getGroupsBy(b => defineGroupByDate(b.BeatmapSet!.DateAdded), items); + + case GroupMode.LastPlayed: + return getGroupsBy(b => + { + DateTimeOffset? maxLastPlayed = aggregateMax(b, items, bb => bb.LastPlayed); + + if (maxLastPlayed == null) + return new GroupDefinition(int.MaxValue, "Never"); + + return defineGroupByDate(maxLastPlayed.Value); + }, items); + + case GroupMode.RankedStatus: + return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items); + + case GroupMode.BPM: + return getGroupsBy(b => + { + double maxBPM = aggregateMax(b, items, bb => bb.BPM); + return defineGroupByBPM(maxBPM); + }, items); case GroupMode.Difficulty: - var starGroup = lastGroup?.Data as StarDifficulty? ?? new StarDifficulty(-1, 0); - double beatmapStarRating = Math.Round(beatmap.StarRating, 2); + return getGroupsBy(b => defineGroupByStars(b.StarRating), items); - if (beatmapStarRating >= starGroup.Stars + 1) + case GroupMode.Length: + return getGroupsBy(b => { - starGroup = new StarDifficulty((int)Math.Floor(beatmapStarRating), 0); + double maxLength = aggregateMax(b, items, bb => bb.Length); + return defineGroupByLength(maxLength); + }, items); - if (starGroup.Stars == 0) - return new GroupDefinition(starGroup, "Below 1 Star"); + case GroupMode.Collections: + // todo: unsupported. + goto case GroupMode.NoGrouping; - if (starGroup.Stars == 1) - return new GroupDefinition(starGroup, "1 Star"); + case GroupMode.Favourites: + // todo: unsupported. + goto case GroupMode.NoGrouping; - return new GroupDefinition(starGroup, $"{starGroup.Stars} Stars"); - } + case GroupMode.MyMaps: + // todo: unsupported. + goto case GroupMode.NoGrouping; - break; + case GroupMode.RankAchieved: + // todo: unsupported. + goto case GroupMode.NoGrouping; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private List getGroupsBy(Func getGroup, List items) + { + return items.GroupBy(i => getGroup((BeatmapInfo)i.Model)) + .OrderBy(s => s.Key.Order) + .Select(g => new GroupMapping(g.Key, g.ToList())) + .ToList(); + } + + private GroupDefinition defineGroupAlphabetically(string name) + { + char firstChar = name.FirstOrDefault(); + + if (char.IsAsciiDigit(firstChar)) + return new GroupDefinition(int.MinValue, "0-9"); + + if (char.IsAsciiLetter(firstChar)) + return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString()); + + return new GroupDefinition(int.MaxValue, "Other"); + } + + private GroupDefinition defineGroupByDate(DateTimeOffset date) + { + var now = DateTimeOffset.Now; + var elapsed = now - date; + + if (elapsed.TotalDays < 1) + return new GroupDefinition(0, "Today"); + + if (elapsed.TotalDays < 2) + return new GroupDefinition(1, "Yesterday"); + + if (elapsed.TotalDays < 7) + return new GroupDefinition(2, "Last week"); + + if (elapsed.TotalDays < 30) + return new GroupDefinition(3, "1 month ago"); + + for (int i = 60; i <= 150; i += 30) + { + if (elapsed.TotalDays < i) + return new GroupDefinition(i, $"{i / 30} months ago"); } - return null; + return new GroupDefinition(int.MaxValue, "Over 5 months ago"); } + + private GroupDefinition defineGroupByStatus(BeatmapOnlineStatus status) + { + switch (status) + { + case BeatmapOnlineStatus.Ranked: + case BeatmapOnlineStatus.Approved: + return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription()); + + case BeatmapOnlineStatus.Qualified: + return new GroupDefinition(1, status.GetDescription()); + + case BeatmapOnlineStatus.WIP: + return new GroupDefinition(2, status.GetDescription()); + + case BeatmapOnlineStatus.Pending: + return new GroupDefinition(3, status.GetDescription()); + + case BeatmapOnlineStatus.Graveyard: + return new GroupDefinition(4, status.GetDescription()); + + case BeatmapOnlineStatus.LocallyModified: + return new GroupDefinition(5, status.GetDescription()); + + case BeatmapOnlineStatus.None: + return new GroupDefinition(6, status.GetDescription()); + + case BeatmapOnlineStatus.Loved: + return new GroupDefinition(7, status.GetDescription()); + + default: + throw new ArgumentOutOfRangeException(nameof(status), status, null); + } + } + + private GroupDefinition defineGroupByBPM(double bpm) + { + for (int i = 1; i < 6; i++) + { + if (bpm < i * 60) + return new GroupDefinition(i, $"Under {i * 60} BPM"); + } + + return new GroupDefinition(6, "Over 300 BPM"); + } + + private GroupDefinition defineGroupByStars(double stars) + { + int starInt = (int)Math.Round(stars, 2); + var groupData = new StarDifficulty(starInt, 0); + + if (starInt == 0) + return new GroupDefinition(0, "Below 1 Star", groupData); + + if (starInt == 1) + return new GroupDefinition(1, "1 Star", groupData); + + return new GroupDefinition(starInt, $"{starInt} Stars", groupData); + } + + private GroupDefinition defineGroupByLength(double length) + { + for (int i = 1; i < 6; i++) + { + if (length <= i * 60_000) + { + if (i == 1) + return new GroupDefinition(1, "1 minute or less"); + + return new GroupDefinition(i, $"{i} minutes or less"); + } + } + + if (length <= 10 * 60_000) + return new GroupDefinition(10, "10 minutes or less"); + + return new GroupDefinition(11, "Over 10 minutes"); + } + + private static T? aggregateMax(BeatmapInfo b, IEnumerable items, Func func) + { + var matchedBeatmaps = items.Select(i => i.Model).Cast().Where(beatmap => beatmap.BeatmapSet!.Equals(b.BeatmapSet)); + return matchedBeatmaps.Max(func); + } + + private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index b042f34d22..f4d5bca1e2 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); var group = (GroupDefinition)Item.Model; - var stars = (StarDifficulty)group.Data; + var stars = (StarDifficulty)group.Data!; int starNumber = (int)stars.Stars; ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); From 2b37e7f26c15eaa7a7c4902054623da8df3c7f44 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 15 May 2025 13:58:23 +0300 Subject: [PATCH 03/17] Add test coverage --- .../BeatmapCarouselFilterGroupingTest.cs | 333 ++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs new file mode 100644 index 0000000000..82c2567021 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -0,0 +1,333 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Carousel; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class BeatmapCarouselFilterGroupingTest + { + #region No grouping + + [Test] + public async Task TestNoGrouping() + { + var beatmapSets = new List(); + addBeatmapSet(applyTitle('E'), beatmapSets, out var beatmap1); + addBeatmapSet(applyArtist('D'), beatmapSets, out var beatmap2); + addBeatmapSet(applyAuthor('H'), beatmapSets, out var beatmap3); + addBeatmapSet(applyLength(65_000), beatmapSets, out var beatmap4); + + BeatmapInfo[] allBeatmaps = + [ + ..beatmap1.Beatmaps, + ..beatmap2.Beatmaps, + ..beatmap3.Beatmaps, + ..beatmap4.Beatmaps + ]; + + var results = await runGrouping(GroupMode.NoGrouping, beatmapSets); + Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); + assertTotal(results, beatmapSets.Count + allBeatmaps.Length); + } + + #endregion + + #region Alphabetical grouping + + [Test] + public async Task TestGroupingByArtist() => await testAlphabeticGroupingMode(GroupMode.Artist, applyArtist); + + [Test] + public async Task TestGroupingByAuthor() => await testAlphabeticGroupingMode(GroupMode.Author, applyAuthor); + + [Test] + public async Task TestGroupingByTitle() => await testAlphabeticGroupingMode(GroupMode.Title, applyTitle); + + private async Task testAlphabeticGroupingMode(GroupMode mode, Func> applyBeatmap) + { + int total = 0; + var beatmapSets = new List(); + + addBeatmapSet(applyBeatmap('4'), beatmapSets, out var fourBeatmap); + addBeatmapSet(applyBeatmap('5'), beatmapSets, out var fiveBeatmap); + addBeatmapSet(applyBeatmap('A'), beatmapSets, out var aBeatmap); + addBeatmapSet(applyBeatmap('F'), beatmapSets, out var fBeatmap); + addBeatmapSet(applyBeatmap('Z'), beatmapSets, out var zBeatmap); + addBeatmapSet(applyBeatmap('-'), beatmapSets, out var dashBeatmap); + addBeatmapSet(applyBeatmap('_'), beatmapSets, out var underscoreBeatmap); + + var results = await runGrouping(mode, beatmapSets); + assertGroup(results, 0, "0-9", new[] { fiveBeatmap, fourBeatmap }, ref total); + assertGroup(results, 1, "A", new[] { aBeatmap }, ref total); + assertGroup(results, 2, "F", new[] { fBeatmap }, ref total); + assertGroup(results, 3, "Z", new[] { zBeatmap }, ref total); + assertGroup(results, 4, "Other", new[] { dashBeatmap, underscoreBeatmap }, ref total); + assertTotal(results, total); + } + + private Action applyArtist(char first) + { + return s => s.Beatmaps[0].Metadata.Artist = $"{first}-artist"; + } + + private Action applyAuthor(char first) + { + return s => s.Beatmaps[0].Metadata.Author.Username = $"{first}-author"; + } + + private Action applyTitle(char first) + { + return s => s.Beatmaps[0].Metadata.Title = $"{first}-title"; + } + + #endregion + + #region Date grouping + + [Test] + public async Task TestGroupingByDateAdded() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddHours(-5), beatmapSets, out var todayBeatmap); + addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddDays(-1), beatmapSets, out var yesterdayBeatmap); + addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddDays(-4), beatmapSets, out var lastWeekBeatmap); + addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddDays(-21), beatmapSets, out var oneMonthBeatmap); + addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var threeMonthBeatmap); + + var results = await runGrouping(GroupMode.DateAdded, beatmapSets); + assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); + assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); + assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); + assertGroup(results, 3, "1 month ago", new[] { oneMonthBeatmap }, ref total); + assertGroup(results, 4, "3 months ago", new[] { threeMonthBeatmap }, ref total); + assertTotal(results, total); + } + + [Test] + public async Task TestGroupingByLastPlayed() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddHours(-5)), beatmapSets, out var todayBeatmap); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddDays(-1)), beatmapSets, out var yesterdayBeatmap); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddDays(-4)), beatmapSets, out var lastWeekBeatmap); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddDays(-21)), beatmapSets, out var oneMonthBeatmap); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddMonths(-2).AddDays(-3)), beatmapSets, out var threeMonthBeatmap); + addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap); + + var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); + assertGroup(results, 0, "Today", new[] { todayBeatmap }, ref total); + assertGroup(results, 1, "Yesterday", new[] { yesterdayBeatmap }, ref total); + assertGroup(results, 2, "Last week", new[] { lastWeekBeatmap }, ref total); + assertGroup(results, 3, "1 month ago", new[] { oneMonthBeatmap }, ref total); + assertGroup(results, 4, "3 months ago", new[] { threeMonthBeatmap }, ref total); + assertGroup(results, 5, "Never", new[] { neverBeatmap }, ref total); + assertTotal(results, total); + } + + [Test] + public async Task TestGroupingByLastPlayed_BeatmapPartiallyPlayed() + { + var set = TestResources.CreateTestBeatmapSetInfo(3); + set.Beatmaps[0].LastPlayed = null; + set.Beatmaps[1].LastPlayed = null; + set.Beatmaps[2].LastPlayed = DateTimeOffset.Now; + + List beatmapSets = new List { set }; + + var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); + int total = 0; + + assertGroup(results, 0, "Today", new[] { set }, ref total); + assertTotal(results, total); + } + + private Action applyLastPlayed(DateTimeOffset? lastPlayed) + { + return s => s.Beatmaps.ForEach(b => b.LastPlayed = lastPlayed); + } + + #endregion + + #region Ranked Status + + [Test] + public async Task TestGroupingByRankedStatus() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Ranked, beatmapSets, out var rankedBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Approved, beatmapSets, out var approvedBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Qualified, beatmapSets, out var qualifiedBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Loved, beatmapSets, out var lovedBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.WIP, beatmapSets, out var wipBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Pending, beatmapSets, out var pendingBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.Graveyard, beatmapSets, out var graveyardBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.None, beatmapSets, out var noneBeatmap); + addBeatmapSet(s => s.Status = BeatmapOnlineStatus.LocallyModified, beatmapSets, out var localBeatmap); + + var results = await runGrouping(GroupMode.RankedStatus, beatmapSets); + assertGroup(results, 0, "Ranked", new[] { rankedBeatmap, approvedBeatmap }, ref total); + assertGroup(results, 1, "Qualified", new[] { qualifiedBeatmap }, ref total); + assertGroup(results, 2, "WIP", new[] { wipBeatmap }, ref total); + assertGroup(results, 3, "Pending", new[] { pendingBeatmap }, ref total); + assertGroup(results, 4, "Graveyard", new[] { graveyardBeatmap }, ref total); + assertGroup(results, 5, "Local", new[] { localBeatmap }, ref total); + assertGroup(results, 6, "Unknown", new[] { noneBeatmap }, ref total); + assertGroup(results, 7, "Loved", new[] { lovedBeatmap }, ref total); + assertTotal(results, total); + } + + #endregion + + #region BPM grouping + + [Test] + public async Task TestGroupingByBPM() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(applyBPM(30), beatmapSets, out var beatmap30); + addBeatmapSet(applyBPM(60), beatmapSets, out var beatmap60); + addBeatmapSet(applyBPM(90), beatmapSets, out var beatmap90); + addBeatmapSet(applyBPM(120), beatmapSets, out var beatmap120); + addBeatmapSet(applyBPM(270), beatmapSets, out var beatmap270); + addBeatmapSet(applyBPM(300), beatmapSets, out var beatmap300); + addBeatmapSet(applyBPM(330), beatmapSets, out var beatmap330); + + var results = await runGrouping(GroupMode.BPM, beatmapSets); + assertGroup(results, 0, "Under 60 BPM", new[] { beatmap30 }, ref total); + assertGroup(results, 1, "Under 120 BPM", new[] { beatmap60, beatmap90 }, ref total); + assertGroup(results, 2, "Under 180 BPM", new[] { beatmap120 }, ref total); + assertGroup(results, 3, "Under 300 BPM", new[] { beatmap270 }, ref total); + assertGroup(results, 4, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total); + assertTotal(results, total); + } + + private Action applyBPM(double bpm) + { + return s => s.Beatmaps.ForEach(b => b.BPM = bpm); + } + + #endregion + + #region Difficulty grouping + + [Test] + public async Task TestGroupingByDifficulty() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(applyStars(0.5), beatmapSets, out var beatmapBelow1); + addBeatmapSet(applyStars(1.9), beatmapSets, out var beatmapAbove1); + addBeatmapSet(applyStars(1.995), beatmapSets, out var beatmapAlmost2); + addBeatmapSet(applyStars(2), beatmapSets, out var beatmap2); + addBeatmapSet(applyStars(2.1), beatmapSets, out var beatmapAbove2); + addBeatmapSet(applyStars(7), beatmapSets, out var beatmap7); + + var results = await runGrouping(GroupMode.Difficulty, beatmapSets); + assertGroup(results, 0, "Below 1 Star", new[] { beatmapBelow1 }, ref total); + assertGroup(results, 1, "1 Star", new[] { beatmapAbove1 }, ref total); + assertGroup(results, 2, "2 Stars", new[] { beatmapAlmost2, beatmap2, beatmapAbove2 }, ref total); + assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); + assertTotal(results, total); + } + + private Action applyStars(double stars) + { + return s => s.Beatmaps.ForEach(b => b.StarRating = stars); + } + + #endregion + + #region Length grouping + + [Test] + public async Task TestGroupingByLength() + { + int total = 0; + + var beatmapSets = new List(); + addBeatmapSet(applyLength(30_000), beatmapSets, out var beatmap30Sec); + addBeatmapSet(applyLength(60_000), beatmapSets, out var beatmap1Min); + addBeatmapSet(applyLength(90_000), beatmapSets, out var beatmap1Min30Sec); + addBeatmapSet(applyLength(120_000), beatmapSets, out var beatmap2Min); + addBeatmapSet(applyLength(300_000), beatmapSets, out var beatmap5Min); + addBeatmapSet(applyLength(360_000), beatmapSets, out var beatmap6Min); + addBeatmapSet(applyLength(600_000), beatmapSets, out var beatmap10Min); + addBeatmapSet(applyLength(630_000), beatmapSets, out var beatmap10Min30Sec); + + var results = await runGrouping(GroupMode.Length, beatmapSets); + assertGroup(results, 0, "1 minute or less", new[] { beatmap30Sec, beatmap1Min }, ref total); + assertGroup(results, 1, "2 minutes or less", new[] { beatmap1Min30Sec, beatmap2Min }, ref total); + assertGroup(results, 2, "5 minutes or less", new[] { beatmap5Min }, ref total); + assertGroup(results, 3, "10 minutes or less", new[] { beatmap6Min, beatmap10Min }, ref total); + assertGroup(results, 4, "Over 10 minutes", new[] { beatmap10Min30Sec }, ref total); + assertTotal(results, total); + } + + private Action applyLength(double length) + { + return s => s.Beatmaps.ForEach(b => b.Length = length); + } + + #endregion + + private static async Task> runGrouping(GroupMode group, List beatmapSets) + { + var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group }); + var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); + return carouselItems; + } + + private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) + { + var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); + if (groupItem == null) + throw new AssertionException($"Expected group at index {index}, but that is out of bounds"); + + var itemsInGroup = items.SkipWhile(i => i != groupItem).Skip(1).TakeWhile(i => i.Model is not GroupDefinition); + + var groupModel = (GroupDefinition)groupItem.Model; + + Assert.That(groupModel.Title, Is.EqualTo(expectedTitle)); + Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmapSets.SelectMany(bs => bs.Beatmaps))); + + totalItems += itemsInGroup.Count() + 1; + } + + private static void assertTotal(List items, int total) + { + Assert.That(items.Count, Is.EqualTo(total)); + } + + private static void addBeatmapSet(Action change, List list, out BeatmapSetInfo added) + { + var set = TestResources.CreateTestBeatmapSetInfo(); + change(set); + list.Add(set); + added = set; + } + } +} From f8cec19f0402f10a7c0bc972c6206fc52b4e86d7 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 18 May 2025 01:31:43 +0300 Subject: [PATCH 04/17] Split beatmap set if either sort or group mode is difficulty --- osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 0286eadbcc..9fae344f34 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.SelectV2 var criteria = getCriteria(); var newItems = new List(); - BeatmapSetsGroupedTogether = criteria.Sort != SortMode.Difficulty; + BeatmapSetsGroupedTogether = criteria.Sort != SortMode.Difficulty && criteria.Group != GroupMode.Difficulty; var groups = getGroups((List)items, criteria); From 8568ac422fa9b152a31509348671365560768297 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 18 May 2025 12:13:07 +0300 Subject: [PATCH 05/17] Fix last played grouping order not stable --- .../BeatmapCarouselFilterGroupingTest.cs | 32 ++++++++++++++++++- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index 82c2567021..57d81904bd 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -160,6 +160,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 assertTotal(results, total); } + [Test] + public async Task TestGroupingByLastPlayed_NeverBelowOverFiveMonthsAgo() + { + List beatmapSets = new List(); + addBeatmapSet(applyLastPlayed(null), beatmapSets, out var neverBeatmap); + addBeatmapSet(applyLastPlayed(DateTimeOffset.Now.AddMonths(-6)), beatmapSets, out var overFiveMonthsBeatmap); + + var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); + int total = 0; + + assertGroup(results, 0, "Over 5 months ago", new[] { overFiveMonthsBeatmap }, ref total); + assertGroup(results, 1, "Never", new[] { neverBeatmap }, ref total); + assertTotal(results, total); + } + private Action applyLastPlayed(DateTimeOffset? lastPlayed) { return s => s.Beatmaps.ForEach(b => b.LastPlayed = lastPlayed); @@ -298,14 +313,29 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group }); var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); + + // sanity check to ensure no detection of two group items with equal order value. + var groups = carouselItems.Select(i => i.Model).OfType(); + + foreach (var header in groups) + { + var sameOrder = groups.FirstOrDefault(g => g != header && g.Order == header.Order); + if (sameOrder != null) + Assert.Fail($"Detected two groups with equal order number: \"{header.Title}\" vs. \"{sameOrder.Title}\""); + } + return carouselItems; } private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) { var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); + if (groupItem == null) - throw new AssertionException($"Expected group at index {index}, but that is out of bounds"); + { + Assert.Fail($"Expected group at index {index}, but that is out of bounds"); + return; + } var itemsInGroup = items.SkipWhile(i => i != groupItem).Skip(1).TakeWhile(i => i.Model is not GroupDefinition); diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 9fae344f34..8f271df860 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -240,7 +240,7 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(i, $"{i / 30} months ago"); } - return new GroupDefinition(int.MaxValue, "Over 5 months ago"); + return new GroupDefinition(151, "Over 5 months ago"); } private GroupDefinition defineGroupByStatus(BeatmapOnlineStatus status) From b60cf08635672ef24d3e161a72de3b44db9ef9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 May 2025 14:14:48 +0200 Subject: [PATCH 06/17] Add failing test case --- .../TestSceneReplayRewinding.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs index b58046c9e9..5216358a8b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs @@ -65,5 +65,61 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep(@"exit player", () => currentPlayer.Exit()); } + + [Test] + public void TestCorrectComboAccountingForConcurrentObjects() + { + Score score = null!; + + var beatmap = new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = + { + new Note + { + StartTime = 500, + Column = 0, + }, + new Note + { + StartTime = 500, + Column = 2, + }, + new HoldNote + { + StartTime = 1000, + EndTime = 1500, + Column = 1, + } + } + }; + + AddStep(@"create replay", () => score = new Score + { + Replay = new Replay + { + Frames = + { + new ManiaReplayFrame(500, ManiaAction.Key1, ManiaAction.Key3), + new ManiaReplayFrame(520), + new ManiaReplayFrame(1000, ManiaAction.Key2), + new ManiaReplayFrame(1500), + } + }, + ScoreInfo = new ScoreInfo() + }); + + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap)); + AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset); + AddStep(@"push player", () => LoadScreen(currentPlayer = new ReplayPlayer(score))); + + AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"wait for objects to be judged", () => currentPlayer.ChildrenOfType().Single().CurrentTime, () => Is.GreaterThan(1600)); + AddStep(@"stop gameplay", () => currentPlayer.ChildrenOfType().Single().Stop()); + AddStep(@"seek to start", () => currentPlayer.Seek(0)); + AddAssert(@"combo is 0", () => currentPlayer.GameplayState.ScoreProcessor.Combo.Value, () => Is.Zero); + + AddStep(@"exit player", () => currentPlayer.Exit()); + } } } From 561578c0e903d2d8cda0228f2019d61c3ca4b95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 May 2025 14:34:45 +0200 Subject: [PATCH 07/17] Add protective test coverage of combo accounting --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 1647fbee42..d6d540996f 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -421,6 +421,59 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); } + [Test] + public void TestComboAccounting() + { + var testBeatmap = new Beatmap + { + HitObjects = Enumerable.Range(1, 40).Select(i => new TestHitObject(HitResult.Perfect, HitResult.Miss)).ToList(), + }; + scoreProcessor.ApplyBeatmap(testBeatmap); + + var results = new List(); + JudgementResult judgementResult; + + for (int i = 0; i < 25; ++i) + { + judgementResult = new JudgementResult(testBeatmap.HitObjects[i], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Perfect + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(i + 1)); + } + + judgementResult = new JudgementResult(testBeatmap.HitObjects[25], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Miss + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + + for (int i = 26; i < 40; ++i) + { + judgementResult = new JudgementResult(testBeatmap.HitObjects[i], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Perfect + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(i - 25)); + } + + Assert.That(scoreProcessor.MaximumStatistics[HitResult.Perfect], Is.EqualTo(40)); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(14)); + Assert.That(scoreProcessor.HighestCombo.Value, Is.EqualTo(25)); + + foreach (var result in Enumerable.Reverse(results)) + scoreProcessor.RevertResult(result); + + Assert.That(scoreProcessor.Combo.Value, Is.Zero); + Assert.That(scoreProcessor.HighestCombo.Value, Is.Zero); + } + private class TestJudgement : Judgement { public override HitResult MaxResult { get; } From 237de1ef72a06babd9e3dbd582d5c29faca171b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 May 2025 14:53:45 +0200 Subject: [PATCH 08/17] Adjust combo accounting in score processor to be order-agnostic Closes https://github.com/ppy/osu/issues/21732. While the problem of multiple judgements in one frame and ordering of `RevertResult()` calls as described in the issue is a real one, this commit is a bit of a sidestep of the entire issue. The idea here that while *snapshots* of instantaneous combo values are susceptible to such ordering foibles on revert, *deltas* are not - and such, when deltas are using to update the combo counts on revert, ordering stops mattering so much. --- osu.Game/Rulesets/Judgements/JudgementResult.cs | 5 +++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 4b98df50d7..ab83ee62b0 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Judgements /// public int HighestComboAtJudgement { get; internal set; } + /// + /// The highest combo achieved after this occurred. + /// + public int HighestComboAfterJudgement { get; internal set; } + /// /// The health prior to this occurring. /// diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7b5af9beda..3663e7f008 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -202,7 +202,6 @@ namespace osu.Game.Rulesets.Scoring { Ruleset = ruleset; - Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += _ => updateRank(); Mods.ValueChanged += mods => @@ -238,7 +237,10 @@ namespace osu.Game.Rulesets.Scoring else if (result.Type.BreaksCombo()) Combo.Value = 0; + HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); + result.ComboAfterJudgement = Combo.Value; + result.HighestComboAfterJudgement = HighestCombo.Value; if (result.Judgement.MaxResult.AffectsAccuracy()) { @@ -281,8 +283,11 @@ namespace osu.Game.Rulesets.Scoring if (!TrackHitEvents) throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled."); - Combo.Value = result.ComboAtJudgement; - HighestCombo.Value = result.HighestComboAtJudgement; + // the reason this is written so funnily rather than just using `ComboAtJudgement` + // is to nullify impact of ordering when reverting concurrent judgement results + // (think mania and multiple judgements within a frame). + Combo.Value -= (result.ComboAfterJudgement - result.ComboAtJudgement); + HighestCombo.Value -= (result.HighestComboAfterJudgement - result.HighestComboAtJudgement); if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed) return; From 1576001491bb64d0bb30f88834bdb2f41b9f60ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 May 2025 14:55:30 +0200 Subject: [PATCH 09/17] Add demonstrative test coverage of combo accounting being order-agnostic --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index d6d540996f..f45422e0c4 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -422,7 +422,7 @@ namespace osu.Game.Tests.Rulesets.Scoring } [Test] - public void TestComboAccounting() + public void TestComboAccounting([Values] bool shuffleResults) { var testBeatmap = new Beatmap { @@ -467,7 +467,13 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(14)); Assert.That(scoreProcessor.HighestCombo.Value, Is.EqualTo(25)); - foreach (var result in Enumerable.Reverse(results)) + // random shuffle is VERY extreme and overkill. + // it might not work correctly for any other `ScoreProcessor` property, and the intermediate results likely make no sense. + // the goal is only to demonstrate idempotency to zero when reverting all results. + var random = new Random(20250519); + var toRevert = shuffleResults ? results.OrderBy(_ => random.Next()).ToList() : Enumerable.Reverse(results); + + foreach (var result in toRevert) scoreProcessor.RevertResult(result); Assert.That(scoreProcessor.Combo.Value, Is.Zero); From d7efce537814c876737c8b958379975bfddec80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 22 May 2025 12:52:40 +0200 Subject: [PATCH 10/17] Do not use unseeded RNG in replay analysis container test It's just bad form. --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 184938ceda..60077b9273 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Replay fabricateReplay() { var frames = new List(); - var random = new Random(); + var random = new Random(20250522); int posX = 250; int posY = 250; From 44feb7814e03a168207414dfe188a073adacd460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 23 May 2025 08:29:45 +0200 Subject: [PATCH 11/17] Use a fixed clock --- .../TestSceneOsuAnalysisContainer.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 60077b9273..06ab6e496f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Replays; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests [Cached] private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo); + private readonly StopwatchClock clock = new StopwatchClock(); + [SetUpSteps] public void SetUpSteps() { @@ -35,7 +38,10 @@ namespace osu.Game.Rulesets.Osu.Tests { new OsuPlayfieldAdjustmentContainer { - Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()) + { + Clock = new FramedClock(clock) + }, }, settings = new ReplayAnalysisSettings(config), }; @@ -55,11 +61,23 @@ namespace osu.Game.Rulesets.Osu.Tests settings.ShowAimMarkers.Value = true; settings.ShowCursorPath.Value = true; }); + AddToggleStep("toggle pause", running => + { + if (running) + clock.Stop(); + else + clock.Start(); + }); } [Test] public void TestHitMarkers() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); @@ -69,6 +87,11 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimMarker() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); @@ -78,6 +101,11 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimLines() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); @@ -109,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests frames.Add(new OsuReplayFrame { - Time = Time.Current + i * 15, + Time = i * 15, Position = new Vector2(posX, posY), Actions = actions.ToList(), }); From ff48c4a047116d7e0674196530e0d220d8c7fbd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 May 2025 19:37:06 +0900 Subject: [PATCH 12/17] Fix typo in comment --- osu.Game/Screens/Select/Filter/GroupMode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index a560c155ae..4025f5d702 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Select.Filter [Description("Title")] Title, - [Obsolete($"Use {nameof(NoGrouping)} instead.")] // todo: remove in 20251018 + [Obsolete($"Use {nameof(NoGrouping)} instead.")] // todo: can be removed after 20251201 All, } } From 6555e45367b3fcad66757524ea53abef484ff822 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 May 2025 19:43:05 +0900 Subject: [PATCH 13/17] Fix weird todo comments.. --- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 8f271df860..f812835986 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -176,19 +176,19 @@ namespace osu.Game.Screens.SelectV2 }, items); case GroupMode.Collections: - // todo: unsupported. + // TODO: needs implementation goto case GroupMode.NoGrouping; case GroupMode.Favourites: - // todo: unsupported. + // TODO: needs implementation goto case GroupMode.NoGrouping; case GroupMode.MyMaps: - // todo: unsupported. + // TODO: needs implementation goto case GroupMode.NoGrouping; case GroupMode.RankAchieved: - // todo: unsupported. + // TODO: needs implementation goto case GroupMode.NoGrouping; default: From dc519441f92399786599d980ebcd6a5a8e0d3678 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 May 2025 19:49:24 +0900 Subject: [PATCH 14/17] Handle star difficulty grouping in a way which doesn't require arbitrary nullable data --- .../Visual/SongSelectV2/TestScenePanelGroup.cs | 8 ++++---- osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 14 +++++++++----- .../SelectV2/BeatmapCarouselFilterGrouping.cs | 8 ++++---- .../Screens/SelectV2/PanelGroupStarDifficulty.cs | 6 ++---- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs index d91e7283d1..13b6456032 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelGroup.cs @@ -56,21 +56,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(0, $"{star} Star(s)", new StarDifficulty(star, 0))) + Item = new CarouselItem(new StarDifficultyGroupDefinition(0, $"{star} Star(s)", new StarDifficulty(star, 0))) }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(1, $"{star} Star(s)", new StarDifficulty(star, 0))), + Item = new CarouselItem(new StarDifficultyGroupDefinition(1, $"{star} Star(s)", new StarDifficulty(star, 0))), KeyboardSelected = { Value = true }, }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(2, $"{star} Star(s)", new StarDifficulty(star, 0))), + Item = new CarouselItem(new StarDifficultyGroupDefinition(2, $"{star} Star(s)", new StarDifficulty(star, 0))), Expanded = { Value = true }, }, new PanelGroupStarDifficulty { - Item = new CarouselItem(new GroupDefinition(3, $"{star} Star(s)", new StarDifficulty(star, 0))), + Item = new CarouselItem(new StarDifficultyGroupDefinition(3, $"{star} Star(s)", new StarDifficulty(star, 0))), Expanded = { Value = true }, KeyboardSelected = { Value = true }, }, diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 70dfbb6f72..0d84dea605 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -411,10 +411,10 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case GroupDefinition group: - if (group.Data is StarDifficulty) - return starsGroupPanelPool.Get(); + case StarDifficultyGroupDefinition: + return starsGroupPanelPool.Get(); + case GroupDefinition: return groupPanelPool.Get(); case BeatmapInfo: @@ -438,6 +438,10 @@ namespace osu.Game.Screens.SelectV2 /// /// The order of this group in the carousel, sorted using ascending order. /// The title of this group. - /// Additional data. Provide a for difficulty groups, or null for any other group. - public record GroupDefinition(int Order, string Title, object? Data = null); + public record GroupDefinition(int Order, string Title); + + /// + /// Defines a grouping header for a set of carousel items grouped by star difficulty. + /// + public record StarDifficultyGroupDefinition(int Order, string Title, StarDifficulty Difficulty) : GroupDefinition(Order, Title); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f812835986..45a983a0ea 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -291,15 +291,15 @@ namespace osu.Game.Screens.SelectV2 private GroupDefinition defineGroupByStars(double stars) { int starInt = (int)Math.Round(stars, 2); - var groupData = new StarDifficulty(starInt, 0); + var starDifficulty = new StarDifficulty(starInt, 0); if (starInt == 0) - return new GroupDefinition(0, "Below 1 Star", groupData); + return new StarDifficultyGroupDefinition(0, "Below 1 Star", starDifficulty); if (starInt == 1) - return new GroupDefinition(1, "1 Star", groupData); + return new StarDifficultyGroupDefinition(1, "1 Star", starDifficulty); - return new GroupDefinition(starInt, $"{starInt} Stars", groupData); + return new StarDifficultyGroupDefinition(starInt, $"{starInt} Stars", starDifficulty); } private GroupDefinition defineGroupByLength(double length) diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs index f4d5bca1e2..6de4d9e387 100644 --- a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; @@ -139,9 +138,8 @@ namespace osu.Game.Screens.SelectV2 Debug.Assert(Item != null); - var group = (GroupDefinition)Item.Model; - var stars = (StarDifficulty)group.Data!; - int starNumber = (int)stars.Stars; + var group = (StarDifficultyGroupDefinition)Item.Model; + int starNumber = (int)group.Difficulty.Stars; ratingColour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); From cd44cab3ff29701ee1adb6aeca8181509420e6b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 24 May 2025 01:01:17 +0900 Subject: [PATCH 15/17] Remove obsoleted flow by using a new configuration variable The old one wasn't used anyway. --- osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs | 2 +- osu.Game/Configuration/OsuConfigManager.cs | 8 ++------ osu.Game/Screens/Select/Filter/GroupMode.cs | 4 ---- osu.Game/Screens/Select/FilterControl.cs | 2 +- .../Screens/SelectV2/BeatmapCarouselFilterGrouping.cs | 3 --- osu.Game/Screens/SelectV2/FilterControl.cs | 6 ++---- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index a757d27a84..c7c56f30f4 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 SelectedMods.SetDefault(); Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title); - Config.SetValue(OsuSetting.SongSelectGroupingMode, GroupMode.NoGrouping); + Config.SetValue(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping); SongSelect = null!; }); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 18d8f69918..8f6fc214e1 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -47,7 +47,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1); - SetDefault(OsuSetting.SongSelectGroupingMode, GroupMode.NoGrouping); + SetDefault(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping); SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title); SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); @@ -263,10 +263,6 @@ namespace osu.Game.Configuration if (RuntimeInfo.IsMobile) GetBindable(OsuSetting.UIScale).SetDefault(); } - - if (combined < 20250518) - // GroupMode.All, the previous default grouping mode, is made obsolete and to be removed in favour of GroupMode.NoGrouping. - GetBindable(OsuSetting.SongSelectGroupingMode).SetDefault(); } public override TrackedSettings CreateTrackedSettings() @@ -393,7 +389,7 @@ namespace osu.Game.Configuration SaveUsername, DisplayStarsMinimum, DisplayStarsMaximum, - SongSelectGroupingMode, + SongSelectGroupMode, SongSelectSortingMode, RandomSelectAlgorithm, ModSelectHotkeyStyle, diff --git a/osu.Game/Screens/Select/Filter/GroupMode.cs b/osu.Game/Screens/Select/Filter/GroupMode.cs index 4025f5d702..862c2300fa 100644 --- a/osu.Game/Screens/Select/Filter/GroupMode.cs +++ b/osu.Game/Screens/Select/Filter/GroupMode.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.ComponentModel; namespace osu.Game.Screens.Select.Filter @@ -49,8 +48,5 @@ namespace osu.Game.Screens.Select.Filter [Description("Title")] Title, - - [Obsolete($"Use {nameof(NoGrouping)} instead.")] // todo: can be removed after 20251201 - All, } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 488f63accb..4781a3dee7 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Select private void load(OsuColour colours, OsuConfigManager config) { sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); - groupMode = config.GetBindable(OsuSetting.SongSelectGroupingMode); + groupMode = config.GetBindable(OsuSetting.SongSelectGroupMode); Children = new Drawable[] { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index 45a983a0ea..c512d1c6bc 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -126,9 +126,6 @@ namespace osu.Game.Screens.SelectV2 { switch (criteria.Group) { -#pragma warning disable CS0618 // Type or member is obsolete - case GroupMode.All: -#pragma warning restore CS0618 // Type or member is obsolete case GroupMode.NoGrouping: return new List { new GroupMapping(null, items) }; diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 036e5c85ca..8b360688fa 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -162,9 +162,7 @@ namespace osu.Game.Screens.SelectV2 groupDropdown = new ShearedDropdown("Group by") { RelativeSizeAxes = Axes.X, -#pragma warning disable CS0618 // Type or member is obsolete - Items = Enum.GetValues().Where(m => m != GroupMode.All), -#pragma warning restore CS0618 // Type or member is obsolete + Items = Enum.GetValues(), }, Empty(), collectionDropdown = new CollectionDropdown @@ -187,7 +185,7 @@ namespace osu.Game.Screens.SelectV2 difficultyRangeSlider.UpperBound = config.GetBindable(OsuSetting.DisplayStarsMaximum); config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active); config.BindWith(OsuSetting.SongSelectSortingMode, sortDropdown.Current); - config.BindWith(OsuSetting.SongSelectGroupingMode, groupDropdown.Current); + config.BindWith(OsuSetting.SongSelectGroupMode, groupDropdown.Current); ruleset.BindValueChanged(_ => updateCriteria()); mods.BindValueChanged(m => From 5e3fd7a42adbb676ffaa84a7b23e9819e6b9160d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 May 2025 04:59:07 +0900 Subject: [PATCH 16/17] Fix rider EAP new naming inspections --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 5 ++++- .../TestSceneClicksPerSecondCalculator.cs | 5 ++++- .../Overlays/Chat/Listing/ChannelListingItem.cs | 6 +++++- .../Overlays/Dashboard/Friends/FriendsList.cs | 5 ++++- osu.Game/Overlays/Mods/ModSelectPanel.cs | 5 ++++- .../Edit/Timing/WaveformComparisonDisplay.cs | 11 +++++++++-- .../Screens/OnlinePlay/FooterButtonFreeMods.cs | 5 ++++- .../Screens/OnlinePlay/FooterButtonFreestyle.cs | 5 ++++- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 1 - .../HUD/JudgementCounter/JudgementCounter.cs | 5 ++++- .../Ranking/UserTagControl_AddTagsPopover.cs | 16 +++++++++++++--- .../BeatmapTitleWedge_DifficultyDisplay.cs | 1 - osu.Game/Screens/SelectV2/Panel.cs | 10 ++++++++-- osu.Game/Utils/SentryLogger.cs | 5 ++++- osu.sln.DotSettings | 3 ++- 15 files changed, 69 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 06cb9c3419..421b908dc9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -43,7 +43,10 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var obj in beatmap.HitObjects.OfType()) { - if (obj.NewCombo) { lastNewComboTime = obj.StartTime; } + if (obj.NewCombo) + { + lastNewComboTime = obj.StartTime; + } applyFadeInAdjustment(obj); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs index 55d57d7a65..9c93eb375c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs @@ -108,7 +108,10 @@ namespace osu.Game.Tests.Visual.Gameplay public bool IsRunning => true; - public double TrueGameplayRate { set => adjustableAudioComponent.Tempo.Value = value; } + public double TrueGameplayRate + { + set => adjustableAudioComponent.Tempo.Value = value; + } private readonly AudioAdjustments adjustableAudioComponent = new AudioAdjustments(); diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs index 466f8b2f5d..539d7c5075 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs @@ -31,7 +31,11 @@ namespace osu.Game.Overlays.Chat.Listing public bool FilteringActive { get; set; } public IEnumerable FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty }; - public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } + + public bool MatchingFilter + { + set => this.FadeTo(value ? 1f : 0f, 100); + } protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs index 955c2c046e..c7689dff8f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsList.cs @@ -207,7 +207,10 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - bool IFilterable.FilteringActive { set { } } + bool IFilterable.FilteringActive + { + set { } + } } } } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 6d48576742..5cf858fc1d 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -300,7 +300,10 @@ namespace osu.Game.Overlays.Mods } } - public bool FilteringActive { set { } } + public bool FilteringActive + { + set { } + } #endregion } diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 2df2dd7c5b..57bf20de43 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -356,8 +356,15 @@ namespace osu.Game.Screens.Edit.Timing waveformGraph.Waveform = beatmap.Value.Waveform; } - public int BeatIndex { set => beatIndexText.Text = value.ToString(); } - public Vector2 WaveformScale { set => waveformGraph.Scale = value; } + public int BeatIndex + { + set => beatIndexText.Text = value.ToString(); + } + + public Vector2 WaveformScale + { + set => waveformGraph.Scale = value; + } public void WaveformOffsetTo(float value, bool animated) => this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint); diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index ad780cd27d..7c632d1619 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -28,7 +28,10 @@ namespace osu.Game.Screens.OnlinePlay protected override bool IsActive => FreeMods.Value.Count > 0; - public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + public new Action Action + { + set => throw new NotSupportedException("The click action is handled by the button itself."); + } private OsuSpriteText count = null!; private Circle circle = null!; diff --git a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs index 6ee983af20..c4edcec976 100644 --- a/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs @@ -21,7 +21,10 @@ namespace osu.Game.Screens.OnlinePlay protected override bool IsActive => Freestyle.Value; - public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); } + public new Action Action + { + set => throw new NotSupportedException("The click action is handled by the button itself."); + } private OsuSpriteText text = null!; private Circle circle = null!; diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 66aa3d9cc0..c7f285f552 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -4,7 +4,6 @@ #nullable disable using System.Collections.Generic; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index d69416f34a..77c03069be 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -20,7 +20,10 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public readonly JudgementCount Result; - public JudgementCounter(JudgementCount result) => Result = result; + public JudgementCounter(JudgementCount result) + { + Result = result; + } public OsuSpriteText ResultName = null!; private FillFlowContainer flowContainer = null!; diff --git a/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs b/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs index 90fd8c19c2..ed4b46ab64 100644 --- a/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs +++ b/osu.Game/Screens/Ranking/UserTagControl_AddTagsPopover.cs @@ -140,7 +140,10 @@ namespace osu.Game.Screens.Ranking set => Alpha = value ? 1 : 0; } - public bool FilteringActive { set { } } + public bool FilteringActive + { + set { } + } public GroupFlow(string? name) { @@ -245,8 +248,15 @@ namespace osu.Game.Screens.Ranking public IEnumerable FilterTerms => [Tag.FullName, Tag.Description]; - public bool MatchingFilter { set => Alpha = value ? 1 : 0; } - public bool FilteringActive { set { } } + public bool MatchingFilter + { + set => Alpha = value ? 1 : 0; + } + + public bool FilteringActive + { + set { } + } protected override void LoadComplete() { diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 9aaf317cb0..4281717816 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/SelectV2/Panel.cs b/osu.Game/Screens/SelectV2/Panel.cs index c22a88a55f..240bede05b 100644 --- a/osu.Game/Screens/SelectV2/Panel.cs +++ b/osu.Game/Screens/SelectV2/Panel.cs @@ -45,9 +45,15 @@ namespace osu.Game.Screens.SelectV2 protected Container Content { get; private set; } = null!; - public Drawable Background { set => backgroundContainer.Child = value; } + public Drawable Background + { + set => backgroundContainer.Child = value; + } - public Drawable Icon { set => iconContainer.Child = value; } + public Drawable Icon + { + set => iconContainer.Child = value; + } private Color4? accentColour; diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 2172ea895e..95086c501f 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -58,7 +58,10 @@ namespace osu.Game.Utils Logger.NewEntry += processLogEntry; } - ~SentryLogger() => Dispose(false); + ~SentryLogger() + { + Dispose(false); + } public void AttachUser(IBindable user) { diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b8a455e2f1..99c42ec6f2 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -304,7 +304,7 @@ 1 1 NEXT_LINE - MULTILINE + DO_NOT_CHANGE True True True @@ -781,6 +781,7 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected, FileLocal" Description="Events"><ElementKinds><Kind Name="EVENT" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> From 48fe2a672340902d76d96873bdcc9592be2ac126 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 May 2025 20:40:45 +0900 Subject: [PATCH 17/17] Add back missing using statement --- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 1 + osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index c7f285f552..66aa3d9cc0 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -4,6 +4,7 @@ #nullable disable using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs index 4281717816..9aaf317cb0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapTitleWedge_DifficultyDisplay.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers;