diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index e7172cacbf..1dd39e5bf9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("beatmap in song select", () => { var songSelect = (SoloSongSelect)Game.ScreenStack.CurrentScreen; - return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is BeatmapSetInfo bsi && bsi.MatchesOnlineID(getImport())); + return songSelect.ChildrenOfType().Single().GetCarouselItems()!.Any(i => i.Model is GroupedBeatmapSet gbs && gbs.BeatmapSet.MatchesOnlineID(getImport())); }); } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs index c8f1c1e017..32a7b89424 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselFilterGroupingTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ]; var results = await runGrouping(GroupMode.None, beatmapSets); - Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(beatmapSets)); + Assert.That(results.Select(r => r.Model).OfType().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets)); Assert.That(results.Select(r => r.Model).OfType(), Is.EquivalentTo(allBeatmaps)); assertTotal(results, beatmapSets.Count + allBeatmaps.Length); } @@ -74,11 +74,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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); + assertGroup(results, 0, "0-9", fiveBeatmap.Beatmaps.Concat(fourBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "A", aBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "F", fBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Z", zBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Other", dashBeatmap.Beatmaps.Concat(underscoreBeatmap.Beatmaps), ref total); assertTotal(results, total); } @@ -115,12 +115,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateAdded = DateTimeOffset.Now.AddMonths(-2).AddDays(-3), beatmapSets, out var twoMonthsAgoBeatmap); 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, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsAgoBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsAgoBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -139,13 +139,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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, "Last month", new[] { lastMonthBeatmap }, ref total); - assertGroup(results, 4, "1 month ago", new[] { oneMonthAgoBeatmap }, ref total); - assertGroup(results, 5, "2 months ago", new[] { twoMonthsBeatmap }, ref total); - assertGroup(results, 6, "Never", new[] { neverBeatmap }, ref total); + assertGroup(results, 0, "Today", todayBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Yesterday", yesterdayBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "Last week", lastWeekBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Last month", lastMonthBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "1 month ago", oneMonthAgoBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "2 months ago", twoMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -162,7 +162,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 var results = await runGrouping(GroupMode.LastPlayed, beatmapSets); int total = 0; - assertGroup(results, 0, "Today", new[] { set }, ref total); + assertGroup(results, 0, "Today", [set.Beatmaps[2]], ref total); + assertGroup(results, 1, "Never", [set.Beatmaps[0], set.Beatmaps[1]], ref total); assertTotal(results, total); } @@ -176,8 +177,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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); + assertGroup(results, 0, "Over 5 months ago", overFiveMonthsBeatmap.Beatmaps, ref total); + assertGroup(results, 1, "Never", neverBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -207,14 +208,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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); + assertGroup(results, 0, "Ranked", rankedBeatmap.Beatmaps.Concat(approvedBeatmap.Beatmaps), ref total); + assertGroup(results, 1, "Qualified", qualifiedBeatmap.Beatmaps, ref total); + assertGroup(results, 2, "WIP", wipBeatmap.Beatmaps, ref total); + assertGroup(results, 3, "Pending", pendingBeatmap.Beatmaps, ref total); + assertGroup(results, 4, "Graveyard", graveyardBeatmap.Beatmaps, ref total); + assertGroup(results, 5, "Local", localBeatmap.Beatmaps, ref total); + assertGroup(results, 6, "Unknown", noneBeatmap.Beatmaps, ref total); + assertGroup(results, 7, "Loved", lovedBeatmap.Beatmaps, ref total); assertTotal(results, total); } @@ -240,12 +241,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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, "60 - 70 BPM", new[] { beatmap59, beatmap60 }, ref total); - assertGroup(results, 2, "90 - 100 BPM", new[] { beatmap90, beatmap95 }, ref total); - assertGroup(results, 3, "270 - 280 BPM", new[] { beatmap269, beatmap270 }, ref total); - assertGroup(results, 4, "290 - 300 BPM", new[] { beatmap299 }, ref total); - assertGroup(results, 5, "Over 300 BPM", new[] { beatmap300, beatmap330 }, ref total); + assertGroup(results, 0, "Under 60 BPM", beatmap30.Beatmaps, ref total); + assertGroup(results, 1, "60 - 70 BPM", (beatmap59.Beatmaps.Concat(beatmap60.Beatmaps)), ref total); + assertGroup(results, 2, "90 - 100 BPM", (beatmap90.Beatmaps.Concat(beatmap95.Beatmaps)), ref total); + assertGroup(results, 3, "270 - 280 BPM", (beatmap269.Beatmaps.Concat(beatmap270.Beatmaps)), ref total); + assertGroup(results, 4, "290 - 300 BPM", beatmap299.Beatmaps, ref total); + assertGroup(results, 5, "Over 300 BPM", (beatmap300.Beatmaps.Concat(beatmap330.Beatmaps)), ref total); assertTotal(results, total); } @@ -272,10 +273,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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, beatmapAlmost2 }, ref total); - assertGroup(results, 2, "2 Stars", new[] { beatmap2, beatmapAbove2 }, ref total); - assertGroup(results, 3, "7 Stars", new[] { beatmap7 }, ref total); + assertGroup(results, 0, "Below 1 Star", beatmapBelow1.Beatmaps, ref total); + assertGroup(results, 1, "1 Star", (beatmapAbove1.Beatmaps.Concat(beatmapAlmost2.Beatmaps)), ref total); + assertGroup(results, 2, "2 Stars", (beatmap2.Beatmaps.Concat(beatmapAbove2.Beatmaps)), ref total); + assertGroup(results, 3, "7 Stars", beatmap7.Beatmaps, ref total); assertTotal(results, total); } @@ -304,11 +305,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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); + assertGroup(results, 0, "1 minute or less", (beatmap30Sec.Beatmaps.Concat(beatmap1Min.Beatmaps)), ref total); + assertGroup(results, 1, "2 minutes or less", (beatmap1Min30Sec.Beatmaps.Concat(beatmap2Min.Beatmaps)), ref total); + assertGroup(results, 2, "5 minutes or less", beatmap5Min.Beatmaps, ref total); + assertGroup(results, 3, "10 minutes or less", (beatmap6Min.Beatmaps.Concat(beatmap10Min.Beatmaps)), ref total); + assertGroup(results, 4, "Over 10 minutes", beatmap10Min30Sec.Beatmaps, ref total); assertTotal(results, total); } @@ -334,10 +335,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.DateRanked = null, beatmapSets, out var beatmapUnranked); var results = await runGrouping(GroupMode.DateRanked, beatmapSets); - assertGroup(results, 0, "2025", new[] { beatmap2025 }, ref total); - assertGroup(results, 1, "2010", new[] { beatmap2010 }, ref total); - assertGroup(results, 2, "2007", new[] { beatmapOct2007, beatmapDec2007 }, ref total); - assertGroup(results, 3, "Unranked", new[] { beatmapUnranked }, ref total); + assertGroup(results, 0, "2025", beatmap2025.Beatmaps, ref total); + assertGroup(results, 1, "2010", beatmap2010.Beatmaps, ref total); + assertGroup(results, 2, "2007", (beatmapOct2007.Beatmaps.Concat(beatmapDec2007.Beatmaps)), ref total); + assertGroup(results, 3, "Unranked", beatmapUnranked.Beatmaps, ref total); assertTotal(results, total); } @@ -357,9 +358,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced); var results = await runGrouping(GroupMode.Source, beatmapSets); - assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total); - assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total); - assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total); + assertGroup(results, 0, "Cool Game", (beatmapCoolGame.Beatmaps.Concat(beatmapCoolGameB.Beatmaps)), ref total); + assertGroup(results, 1, "Nice Movie", beatmapNiceMovie.Beatmaps, ref total); + assertGroup(results, 2, "Unsourced", beatmapUnsourced.Beatmaps, ref total); assertTotal(results, total); } @@ -375,7 +376,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); } - private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmapSets, ref int totalItems) + private static void assertGroup(List items, int index, string expectedTitle, IEnumerable expectedBeatmaps, ref int totalItems) { var groupItem = items.Where(i => i.Model is GroupDefinition).ElementAtOrDefault(index); @@ -390,7 +391,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 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))); + Assert.That(itemsInGroup.Select(i => i.Model).OfType(), Is.EquivalentTo(expectedBeatmaps)); totalItems += itemsInGroup.Count() + 1; } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index bc507fbffa..f18e1e9b52 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Using groupingFilter.SetItems.Count alone doesn't work. // When sorting by difficulty, there can be more than one set panel for the same set displayed. - return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is BeatmapSetInfo)); + return groupingFilter.SetItems.Sum(s => s.Value.Count(i => i.Model is GroupedBeatmapSet)); }, () => Is.EqualTo(expected)); } @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo; public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet; - public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet; + public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet; public new GroupDefinition? ExpandedGroup => base.ExpandedGroup; public TestBeatmapCarousel() diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 78c12e2730..687c4c23be 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -396,7 +396,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectNextPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -413,7 +415,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter first away", c => c.UserStarDifficulty.Min = 3); SelectPrevPanel(); - AddAssert("keyboard selected is last set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is last set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } [Test] @@ -428,7 +432,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 ApplyToFilterAndWaitForFilter("filter last set away", c => c.SearchText = BeatmapSets.First().Metadata.Title); SelectPrevPanel(); - AddAssert("keyboard selected is first set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.First())); + AddAssert("keyboard selected is first set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.First())); } [Test] @@ -444,7 +450,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 // Single result is automatically selected for us, so we iterate once backwards to the set header. SelectPrevPanel(); - AddAssert("keyboard selected is second set", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(BeatmapSets.Last())); + AddAssert("keyboard selected is second set", + () => (GetKeyboardSelectedPanel()?.Item?.Model as GroupedBeatmapSet)?.BeatmapSet, + () => Is.EqualTo(BeatmapSets.Last())); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs new file mode 100644 index 0000000000..fa635f9bde --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselSetsSplitApart.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + [TestFixture] + public partial class TestSceneBeatmapCarouselSetsSplitApart : BeatmapCarouselTestScene + { + [SetUpSteps] + public void SetUpSteps() + { + RemoveAllBeatmaps(); + CreateCarousel(); + + SortAndGroupBy(SortMode.Title, GroupMode.Length); + } + + [Test] + public void TestSetTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextSet(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevSet(); + WaitForSetSelection(set: 5, diff: 0); + + SelectPrevSet(); + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 2, diff: 4); + AddAssert("only two beatmap panels visible", () => GetVisiblePanels().Count(), () => Is.EqualTo(2)); + } + + [Test] + public void TestBeatmapTraversal() + { + AddBeatmaps(3, splitApart: true); + AddBeatmaps(3, splitApart: false); + WaitForDrawablePanels(); + + SelectNextSet(); + WaitForSetSelection(set: 0, diff: 0); + + SelectNextPanel(); + WaitForSetSelection(set: 0, diff: 1); + + SelectNextPanel(); // header of set 1 in group 0 + Select(); + WaitForSetSelection(set: 1, diff: 0); + + SelectPrevPanel(); // header of set 1 in group 0 + SelectPrevPanel(); // header of set 0 in group 0 + Select(); + WaitForSetSelection(set: 0, diff: 0); + + SelectPrevPanel(); // header of set 0 in group 0 + SelectPrevPanel(); // header of group 0 + SelectPrevPanel(); // header of group 2 + Select(); + SelectNextPanel(); // header of set 0 in group 2 + Select(); + WaitForSetSelection(set: 0, diff: 4); + } + + [Test] + public void TestRandomStaysInGroup() + { + AddBeatmaps(2, splitApart: false); + AddBeatmaps(1, splitApart: true); + WaitForDrawablePanels(); + + SelectPrevSet(); + SelectPrevSet(); + WaitForSetSelection(set: 1); + WaitForExpandedGroup(2); + + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + AddStep("select next random", () => Carousel.NextRandom()); + WaitForExpandedGroup(2); + } + + protected void AddBeatmaps(int count, bool splitApart) => AddStep($"add {count} beatmaps ({(splitApart ? "" : "not ")}split apart)", () => + { + var beatmapSets = new List(); + + for (int i = 0; i < count; i++) + { + var beatmapSet = CreateTestBeatmapSetInfo(6, false); + + for (int j = 0; j < beatmapSet.Beatmaps.Count; j++) + { + beatmapSet.Beatmaps[j].Length = splitApart ? 30_000 * (j + 1) : 180_000; + } + + beatmapSets.Add(beatmapSet); + } + + BeatmapSets.AddRange(beatmapSets); + }); + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs index 1723185b1f..b574262d55 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestScenePanelSet.cs @@ -75,21 +75,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet) + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)) }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), Expanded = { Value = true } }, new PanelBeatmapSet { - Item = new CarouselItem(beatmapSet), + Item = new CarouselItem(new GroupedBeatmapSet(null, beatmapSet)), KeyboardSelected = { Value = true }, Expanded = { Value = true } }, diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs index 0f7c42946d..0772607a57 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectGrouping.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("no-collection group present", () => { var group = grouping.GroupItems.Single(g => g.Key.Title == "Not in collection"); - return group.Value.Select(i => i.Model).OfType().Single().Equals(beatmapSet); + return group.Value.Select(i => i.Model).OfType().Single().BeatmapSet.Equals(beatmapSet); }); AddStep("add beatmap to collection", () => diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 31583bf8b7..eb9faa5930 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Screens.Gameplay; @@ -66,6 +67,6 @@ namespace osu.Game.Tournament.Tests.Screens () => this.ChildrenOfType().All(score => score.Alpha == (visible ? 1 : 0))); private void toggleWarmup() - => AddStep("toggle warmup", () => this.ChildrenOfType().First().TriggerClick()); + => AddStep("toggle warmup", () => this.ChildrenOfType().First().ChildrenOfType().First().TriggerClick()); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 727db913e2..477372b97d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -56,6 +56,9 @@ namespace osu.Game.Rulesets.Mods { var bindable = (IBindable)property.GetValue(this)!; + if (bindable.IsDefault) + continue; + string valueText; switch (bindable) @@ -69,8 +72,7 @@ namespace osu.Game.Rulesets.Mods break; } - if (!bindable.IsDefault) - yield return (attr.Label, valueText); + yield return (attr.Label, valueText); } } } diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 22d2f41b82..98a7999065 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); - yield return ("Direction", Direction.Value.GetDescription()); + if (!SpinSpeed.IsDefault) + yield return ("Roll speed", $"{SpinSpeed.Value:N2} rpm"); + if (!Direction.IsDefault) + yield return ("Direction", Direction.Value.GetDescription()); } } @@ -55,7 +57,8 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); + playfieldAdjustmentContainer.Rotation = + CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 8dfe8444e8..049b8f9b7f 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Mods { get { - yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); + if (!InitialRate.IsDefault || !FinalRate.IsDefault) + yield return ("Speed change", $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"); if (!AdjustPitch.IsDefault) yield return ("Adjust pitch", AdjustPitch.Value ? "On" : "Off"); diff --git a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs index efc10f26e1..e01df1428c 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PooledDrawableWithLifetimeContainer.cs @@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Objects.Pooling if (!IsPresent) return false; - bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + bool aliveChanged = lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + aliveChanged |= base.CheckChildrenLife(); return aliveChanged; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index a4e957a1bf..c2711ceef0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -69,11 +70,11 @@ namespace osu.Game.Screens.SelectV2 if (grouping.BeatmapSetsGroupedTogether) { // Give some space around the expanded beatmap set, at the top.. - if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded) + if (bottom.Model is GroupedBeatmapSet && bottom.IsExpanded) return SPACING * 2; // ..and the bottom. - if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo) + if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet) return SPACING * 2; // Beatmap difficulty panels do not overlap with themselves or any other panel. @@ -209,12 +210,12 @@ namespace osu.Game.Screens.SelectV2 return true; } - if (item.Model is BeatmapSetInfo beatmapSetInfo) + if (item.Model is GroupedBeatmapSet groupedSet) { - if (oldItems.Contains(beatmapSetInfo)) + if (oldItems.Contains(groupedSet.BeatmapSet)) return false; - RequestRecommendedSelection(beatmapSetInfo.Beatmaps); + RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps); return true; } } @@ -285,7 +286,7 @@ namespace osu.Game.Screens.SelectV2 protected GroupDefinition? ExpandedGroup { get; private set; } - protected BeatmapSetInfo? ExpandedBeatmapSet { get; private set; } + protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; } protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) => grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo; @@ -313,8 +314,8 @@ namespace osu.Game.Screens.SelectV2 return; - case BeatmapSetInfo setInfo: - selectRecommendedDifficultyForBeatmapSet(setInfo); + case GroupedBeatmapSet groupedSet: + selectRecommendedDifficultyForBeatmapSet(groupedSet); return; case BeatmapInfo beatmapInfo: @@ -340,7 +341,7 @@ namespace osu.Game.Screens.SelectV2 switch (model) { - case BeatmapSetInfo: + case GroupedBeatmapSet: case GroupDefinition: throw new InvalidOperationException("Groups should never become selected"); @@ -351,7 +352,7 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(containingGroup); if (grouping.BeatmapSetsGroupedTogether) - setExpandedSet(beatmapInfo); + setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!)); break; } } @@ -426,10 +427,10 @@ namespace osu.Game.Screens.SelectV2 setExpandedGroup(groupForReselection); } - private void selectRecommendedDifficultyForBeatmapSet(BeatmapSetInfo beatmapSet) + private void selectRecommendedDifficultyForBeatmapSet(GroupedBeatmapSet set) { // Selecting a set isn't valid – let's re-select the first visible difficulty. - if (grouping.SetItems.TryGetValue(beatmapSet, out var items)) + if (grouping.SetItems.TryGetValue(set, out var items)) { var beatmaps = items.Select(i => i.Model).OfType(); RequestRecommendedSelection(beatmaps); @@ -477,7 +478,7 @@ namespace osu.Game.Screens.SelectV2 { switch (item.Model) { - case BeatmapSetInfo: + case GroupedBeatmapSet: return true; case BeatmapInfo: @@ -516,11 +517,11 @@ namespace osu.Game.Screens.SelectV2 i.IsExpanded = true; break; - case BeatmapSetInfo set: + case GroupedBeatmapSet groupedSet: // Case where there are set headers, header should be visible // and items should use the set's expanded state. i.IsVisible = true; - setExpansionStateOfSetItems(set, i.IsExpanded); + setExpansionStateOfSetItems(groupedSet, i.IsExpanded); break; default: @@ -550,21 +551,21 @@ namespace osu.Game.Screens.SelectV2 } } - private void setExpandedSet(BeatmapInfo beatmapInfo) + private void setExpandedSet(GroupedBeatmapSet set) { if (ExpandedBeatmapSet != null) setExpansionStateOfSetItems(ExpandedBeatmapSet, false); - ExpandedBeatmapSet = beatmapInfo.BeatmapSet!; + ExpandedBeatmapSet = set; setExpansionStateOfSetItems(ExpandedBeatmapSet, true); } - private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded) + private void setExpansionStateOfSetItems(GroupedBeatmapSet set, bool expanded) { if (grouping.SetItems.TryGetValue(set, out var items)) { foreach (var i in items) { - if (i.Model is BeatmapSetInfo) + if (i.Model is GroupedBeatmapSet) i.IsExpanded = expanded; else i.IsVisible = expanded; @@ -602,7 +603,7 @@ namespace osu.Game.Screens.SelectV2 sampleToggleGroup?.Play(); return; - case BeatmapSetInfo: + case GroupedBeatmapSet: sampleChangeSet?.Play(); return; @@ -741,8 +742,8 @@ namespace osu.Game.Screens.SelectV2 // it is doing a Replace operation on the list. If it is, then check the local handling in beatmapSetsChanged // before changing matching requirements here. - if (x is BeatmapSetInfo beatmapSetX && y is BeatmapSetInfo beatmapSetY) - return beatmapSetX.Equals(beatmapSetY); + if (x is GroupedBeatmapSet groupedSetX && y is GroupedBeatmapSet groupedSetY) + return groupedSetX.Equals(groupedSetY); if (x is BeatmapInfo beatmapX && y is BeatmapInfo beatmapY) return beatmapX.Equals(beatmapY); @@ -772,7 +773,7 @@ namespace osu.Game.Screens.SelectV2 return beatmapPanelPool.Get(); - case BeatmapSetInfo: + case GroupedBeatmapSet: return setPanelPool.Get(); } @@ -882,30 +883,31 @@ namespace osu.Game.Screens.SelectV2 private bool nextRandomSet() { - ICollection visibleSets = ExpandedGroup != null + ICollection visibleSetsUnderGrouping = ExpandedGroup != null // In the case of grouping, users expect random to only operate on the expanded group. // This is going to incur some overhead as we don't have a group-beatmapset mapping currently. // // If this becomes an issue, we could either store a mapping, or run the random algorithm many times // using the `SetItems` method until we get a group HIT. - ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() + ? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType().ToArray() // This is the fastest way to retrieve sets for randomisation. : grouping.SetItems.Keys; - BeatmapSetInfo set; + GroupedBeatmapSet set; switch (randomAlgorithm.Value) { case RandomSelectAlgorithm.RandomPermutation: { - ICollection notYetVisitedSets = visibleSets.Except(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!)).ToList(); + ICollection notYetVisitedSets = + visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList(); if (!notYetVisitedSets.Any()) { - previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSets.Contains(b.BeatmapSet!)); - notYetVisitedSets = visibleSets; + previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!))); + notYetVisitedSets = visibleSetsUnderGrouping; if (CurrentSelection is BeatmapInfo beatmapInfo) - notYetVisitedSets = notYetVisitedSets.Except([beatmapInfo.BeatmapSet!]).ToList(); + notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList(); } if (notYetVisitedSets.Count == 0) @@ -916,7 +918,7 @@ namespace osu.Game.Screens.SelectV2 } case RandomSelectAlgorithm.Random: - set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); + set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count)); break; default: @@ -1013,4 +1015,10 @@ namespace osu.Game.Screens.SelectV2 /// 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); + + /// + /// Used to represent a portion of a under a . + /// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it. + /// + public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet); } diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index f17281db2f..0d2489c304 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -29,21 +29,22 @@ 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 => setMap; + 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 => groupMap; - private Dictionary> setMap = new Dictionary>(); + private Dictionary> setMap = new Dictionary>(); private Dictionary> groupMap = new Dictionary>(); private readonly Func getCriteria; private readonly Func> getCollections; private readonly Func> getLocalUserTopRanks; - public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, Func> getLocalUserTopRanks) + public BeatmapCarouselFilterGrouping(Func getCriteria, Func> getCollections, + Func> getLocalUserTopRanks) { this.getCriteria = getCriteria; this.getCollections = getCollections; @@ -55,7 +56,7 @@ namespace osu.Game.Screens.SelectV2 return await Task.Run(() => { // preallocate space for the new mappings using last known estimates - var newSetMap = new Dictionary>(setMap.Count); + var newSetMap = new Dictionary>(setMap.Count); var newGroupMap = new Dictionary>(groupMap.Count); var criteria = getCriteria(); @@ -93,11 +94,12 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)item.Model; bool newBeatmapSet = lastBeatmap?.BeatmapSet!.ID != beatmap.BeatmapSet!.ID; + var groupedBeatmapSet = new GroupedBeatmapSet(group, beatmap.BeatmapSet!); if (newBeatmapSet) { - if (!newSetMap.TryGetValue(beatmap.BeatmapSet!, out currentSetItems)) - newSetMap[beatmap.BeatmapSet!] = currentSetItems = new HashSet(); + if (!newSetMap.TryGetValue(groupedBeatmapSet, out currentSetItems)) + newSetMap[groupedBeatmapSet] = currentSetItems = new HashSet(); } if (BeatmapSetsGroupedTogether) @@ -107,7 +109,7 @@ namespace osu.Game.Screens.SelectV2 if (groupItem != null) groupItem.NestedItemCount++; - addItem(new CarouselItem(beatmap.BeatmapSet!) + addItem(new CarouselItem(groupedBeatmapSet) { DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 @@ -134,7 +136,7 @@ namespace osu.Game.Screens.SelectV2 currentGroupItems?.Add(i); currentSetItems?.Add(i); - i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is BeatmapSetInfo || !BeatmapSetsGroupedTogether)); + i.IsVisible = i.Model is GroupDefinition || (group == null && (i.Model is GroupedBeatmapSet || !BeatmapSetsGroupedTogether)); } } @@ -189,9 +191,6 @@ namespace osu.Game.Screens.SelectV2 { var date = b.LastPlayed; - if (BeatmapSetsGroupedTogether) - date = aggregateMax(b, static b => b.LastPlayed ?? DateTimeOffset.MinValue); - if (date == null || date == DateTimeOffset.MinValue) return new GroupDefinition(int.MaxValue, "Never"); @@ -202,29 +201,13 @@ namespace osu.Game.Screens.SelectV2 return getGroupsBy(b => defineGroupByStatus(b.BeatmapSet!.Status), items); case GroupMode.BPM: - return getGroupsBy(b => - { - double bpm = FormatUtils.RoundBPM(b.BPM); - - if (BeatmapSetsGroupedTogether) - bpm = aggregateMax(b, bb => FormatUtils.RoundBPM(bb.BPM)); - - return defineGroupByBPM(bpm); - }, items); + return getGroupsBy(b => defineGroupByBPM(FormatUtils.RoundBPM(b.BPM)), items); case GroupMode.Difficulty: return getGroupsBy(b => defineGroupByStars(b.StarRating), items); case GroupMode.Length: - return getGroupsBy(b => - { - double length = b.Length; - - if (BeatmapSetsGroupedTogether) - length = aggregateMax(b, bb => bb.Length); - - return defineGroupByLength(length); - }, items); + return getGroupsBy(b => defineGroupByLength(b.Length), items); case GroupMode.Source: return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items); @@ -433,12 +416,6 @@ namespace osu.Game.Screens.SelectV2 return new GroupDefinition(int.MaxValue, "Unplayed"); } - private static T? aggregateMax(BeatmapInfo b, Func func) - { - var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden); - return beatmaps.Max(func); - } - private record GroupMapping(GroupDefinition? Group, List ItemsInGroup); } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index d776ab1ffb..1a6e886cb7 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -67,6 +67,15 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private IBindable ruleset { get; set; } = null!; + private GroupedBeatmapSet groupedBeatmapSet + { + get + { + Debug.Assert(Item != null); + return (GroupedBeatmapSet)Item!.Model; + } + } + public PanelBeatmapSet() { PanelXOffset = 20f; @@ -179,9 +188,7 @@ namespace osu.Game.Screens.SelectV2 { base.PrepareForUse(); - Debug.Assert(Item != null); - - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = groupedBeatmapSet.BeatmapSet; // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). setBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); @@ -215,7 +222,7 @@ namespace osu.Game.Screens.SelectV2 if (Item == null) return Array.Empty(); - var beatmapSet = (BeatmapSetInfo)Item.Model; + var beatmapSet = groupedBeatmapSet.BeatmapSet; List items = new List(); @@ -268,9 +275,7 @@ namespace osu.Game.Screens.SelectV2 private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - var beatmapSet = (BeatmapSetInfo)Item!.Model; - - Debug.Assert(beatmapSet != null); + var beatmapSet = groupedBeatmapSet.BeatmapSet; TernaryState state;