mirror of
https://github.com/ppy/osu.git
synced 2026-05-21 11:30:08 +08:00
Merge branch 'master' into song-select-better-debounce
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.829.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.903.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -294,6 +294,46 @@ namespace osu.Game.Tests.Skins.IO
|
||||
|
||||
#endregion
|
||||
|
||||
/// <remarks>
|
||||
/// Note that this test passing / failing is platform / OS-specific (if it is to fail, it'll fail on windows).
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public async Task TestExternallyMountingImportWithInvalidFilename()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var zipStream = new MemoryStream();
|
||||
using var zip = ZipArchive.Create();
|
||||
zip.AddEntry("test?.png", new MemoryStream(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }));
|
||||
zip.SaveTo(zipStream);
|
||||
|
||||
var import = await loadSkinIntoOsu(osu, new ImportTask(zipStream, "test skin.osk"));
|
||||
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
var externalEdit = await skinManager.BeginExternalEditing(import.PerformRead(s => s.Detach())); // should not fail
|
||||
|
||||
Assert.That(Directory.Exists(externalEdit.MountedPath));
|
||||
Assert.That(new DirectoryInfo(externalEdit.MountedPath).GetFiles().Select(f => f.Name), Is.EquivalentTo(new[]
|
||||
{
|
||||
"skin.ini",
|
||||
"test.png"
|
||||
}));
|
||||
|
||||
Task finishTask = Task.CompletedTask;
|
||||
host.UpdateThread.Scheduler.Add(() => finishTask = externalEdit.Finish());
|
||||
await finishTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertCorrectMetadata(Live<SkinInfo> import1, string name, string creator, decimal version, OsuGameBase osu)
|
||||
{
|
||||
import1.PerformRead(i =>
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private void setupUserSettings()
|
||||
{
|
||||
AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen());
|
||||
AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentSelection != null);
|
||||
AddUntilStep("Song select has selection", () => songSelect.Carousel?.CurrentGroupedBeatmap != null);
|
||||
AddStep("Set default user settings", () =>
|
||||
{
|
||||
SelectedMods.Value = new[] { new OsuModNoFail() };
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
var results = await runGrouping(GroupMode.None, beatmapSets);
|
||||
Assert.That(results.Select(r => r.Model).OfType<GroupedBeatmapSet>().Select(groupedSet => groupedSet.BeatmapSet), Is.EquivalentTo(beatmapSets));
|
||||
Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps));
|
||||
Assert.That(results.Select(r => r.Model).OfType<GroupedBeatmap>().Select(groupedBeatmap => groupedBeatmap.Beatmap), Is.EquivalentTo(allBeatmaps));
|
||||
assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
|
||||
}
|
||||
|
||||
@@ -391,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<BeatmapInfo>(), Is.EquivalentTo(expectedBeatmaps));
|
||||
Assert.That(itemsInGroup.Select(i => i.Model).OfType<GroupedBeatmap>().Select(gb => gb.Beatmap), Is.EquivalentTo(expectedBeatmaps));
|
||||
|
||||
totalItems += itemsInGroup.Count() + 1;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,13 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
@@ -115,13 +117,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
NewItemsPresented = _ => NewItemsPresentedInvocationCount++,
|
||||
RequestSelection = b =>
|
||||
{
|
||||
BeatmapRequestedSelections.Push(b);
|
||||
Carousel.CurrentSelection = b;
|
||||
BeatmapRequestedSelections.Push(b.Beatmap);
|
||||
Carousel.CurrentGroupedBeatmap = b;
|
||||
},
|
||||
RequestRecommendedSelection = beatmaps =>
|
||||
RequestRecommendedSelection = groupedBeatmaps =>
|
||||
{
|
||||
BeatmapSetRequestedSelections.Push(beatmaps.First().BeatmapSet!);
|
||||
Carousel.CurrentSelection = BeatmapRecommendationFunction?.Invoke(beatmaps) ?? beatmaps.First();
|
||||
var recommendedBeatmap = BeatmapRecommendationFunction?.Invoke(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap;
|
||||
var recommendedGroupedBeatmap = groupedBeatmaps.First(gb => gb.Beatmap.Equals(recommendedBeatmap));
|
||||
BeatmapSetRequestedSelections.Push(recommendedBeatmap.BeatmapSet!);
|
||||
Carousel.CurrentGroupedBeatmap = recommendedGroupedBeatmap;
|
||||
},
|
||||
BleedTop = 50,
|
||||
BleedBottom = 50,
|
||||
@@ -215,8 +219,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentGroupedBeatmap, () => Is.Not.Null);
|
||||
|
||||
protected void CheckRequestPresentCount(int expected) =>
|
||||
AddAssert($"check present count is {expected}", () => Carousel.RequestPresentBeatmapCount, () => Is.EqualTo(expected));
|
||||
@@ -281,8 +285,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
// offset by one because the group itself is included in the items list.
|
||||
CarouselItem item = groupingFilter.GroupItems[groupDefinition].ElementAt(panel + 1);
|
||||
|
||||
return (Carousel.CurrentSelection as BeatmapInfo)?
|
||||
.Equals(item.Model as BeatmapInfo) == true;
|
||||
return Carousel.CurrentGroupedBeatmap?.Equals(item.Model as GroupedBeatmap) == true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,12 +294,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
if (diff != null)
|
||||
{
|
||||
AddUntilStep($"selected is set{set} diff{diff.Value}",
|
||||
() => (Carousel.CurrentSelection as BeatmapInfo),
|
||||
() => Carousel.CurrentBeatmap,
|
||||
() => Is.EqualTo(BeatmapSets[set].Beatmaps[diff.Value]));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection));
|
||||
AddUntilStep($"selected is set{set}", () => BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentBeatmap!));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +418,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
tracked: {Carousel.ItemsTracked}
|
||||
displayable: {Carousel.DisplayableItems}
|
||||
displayed: {Carousel.VisibleItems}
|
||||
selected: {Carousel.CurrentSelection}
|
||||
selected: {Carousel.CurrentGroupedBeatmap}
|
||||
""");
|
||||
|
||||
void createHeader(string text)
|
||||
@@ -437,12 +440,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
public IEnumerable<BeatmapInfo> PostFilterBeatmaps = null!;
|
||||
|
||||
public BeatmapInfo? SelectedBeatmapInfo => CurrentSelection as BeatmapInfo;
|
||||
public BeatmapInfo? SelectedBeatmapInfo => (CurrentSelection as GroupedBeatmap)?.Beatmap;
|
||||
public BeatmapSetInfo? SelectedBeatmapSet => SelectedBeatmapInfo?.BeatmapSet;
|
||||
|
||||
public new GroupedBeatmapSet? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
|
||||
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
|
||||
|
||||
public Func<List<BeatmapCollection>> AllCollections { get; set; } = () => [];
|
||||
public Func<FilterCriteria, Dictionary<Guid, ScoreRank>> BeatmapInfoGuidToTopRankMapping { get; set; } = _ => new Dictionary<Guid, ScoreRank>();
|
||||
|
||||
public TestBeatmapCarousel()
|
||||
{
|
||||
RequestPresentBeatmap = _ => RequestPresentBeatmapCount++;
|
||||
@@ -461,9 +467,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
if (FilterDelay != 0)
|
||||
await Task.Delay(FilterDelay).ConfigureAwait(true);
|
||||
|
||||
PostFilterBeatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
|
||||
PostFilterBeatmaps = items.Select(i => i.Model).OfType<GroupedBeatmap>().Select(i => i.Beatmap);
|
||||
return items;
|
||||
}
|
||||
|
||||
protected override List<BeatmapCollection> GetAllCollections() => AllCollections.Invoke();
|
||||
protected override Dictionary<Guid, ScoreRank> GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => BeatmapInfoGuidToTopRankMapping.Invoke(criteria);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
@@ -81,7 +80,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
@@ -92,9 +91,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
@@ -132,7 +131,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
WaitForBeatmapSelection(0, 1);
|
||||
// Expanding a group will move keyboard selection to the selected beatmap if contained.
|
||||
AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True);
|
||||
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<BeatmapInfo>);
|
||||
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<GroupedBeatmap>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Collections;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselCollectionGrouping : BeatmapCarouselTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
CreateCarousel();
|
||||
|
||||
AddBeatmaps(10, 3);
|
||||
|
||||
AddStep("set up collections", () =>
|
||||
{
|
||||
List<BeatmapCollection> collections =
|
||||
[
|
||||
new BeatmapCollection("collection one", [
|
||||
..BeatmapSets[0].Beatmaps.Select(b => b.MD5Hash),
|
||||
..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash),
|
||||
..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash),
|
||||
BeatmapSets[5].Beatmaps[1].MD5Hash,
|
||||
BeatmapSets[8].Beatmaps[0].MD5Hash,
|
||||
]),
|
||||
new BeatmapCollection("collection two", [
|
||||
BeatmapSets[0].Beatmaps[0].MD5Hash,
|
||||
..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash),
|
||||
..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash),
|
||||
BeatmapSets[6].Beatmaps[2].MD5Hash,
|
||||
BeatmapSets[8].Beatmaps[2].MD5Hash,
|
||||
]),
|
||||
new BeatmapCollection("collection one copy", [
|
||||
..BeatmapSets[0].Beatmaps.Select(b => b.MD5Hash),
|
||||
..BeatmapSets[1].Beatmaps.Select(b => b.MD5Hash),
|
||||
..BeatmapSets[2].Beatmaps.Select(b => b.MD5Hash),
|
||||
BeatmapSets[5].Beatmaps[1].MD5Hash,
|
||||
BeatmapSets[8].Beatmaps[0].MD5Hash,
|
||||
]),
|
||||
];
|
||||
Carousel.AllCollections = () => collections;
|
||||
});
|
||||
|
||||
SortAndGroupBy(SortMode.Title, GroupMode.Collections);
|
||||
WaitForDrawablePanels();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleCopiesOfBeatmapsPresent()
|
||||
{
|
||||
CheckDisplayedGroupsCount(4); // one for each collection, plus no collections
|
||||
// all three collections have beatmaps from 5 beatmap sets
|
||||
// 7 beatmap sets have beatmaps which belong to no collection
|
||||
CheckDisplayedBeatmapSetsCount(5 + 5 + 5 + 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
@@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
@@ -82,9 +81,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
@@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
WaitForBeatmapSelection(0, 0);
|
||||
// Expanding a group will move keyboard selection to the selected beatmap if contained.
|
||||
AddAssert("keyboard selected panel is expanded", () => groupPanel?.Expanded.Value, () => Is.True);
|
||||
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<BeatmapInfo>);
|
||||
AddAssert("keyboard selected panel is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, Is.TypeOf<GroupedBeatmap>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -199,7 +198,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
}
|
||||
|
||||
private void checkBeatmapIsKeyboardSelected() =>
|
||||
AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("check keyboard selected group is beatmap", () => GetKeyboardSelectedPanel()?.Item?.Model, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
|
||||
private void checkGroupKeyboardSelected(int index) => AddUntilStep($"check keyboard selected group is {index}", () => GetKeyboardSelectedPanel()?.Item?.Model, () =>
|
||||
{
|
||||
|
||||
@@ -130,14 +130,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
AddStep("record selection", () => selectedID = Carousel.CurrentBeatmap!.ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID);
|
||||
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +177,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
SelectNextSet();
|
||||
|
||||
AddStep("record selection", () => selectedID = ((BeatmapInfo)Carousel.CurrentSelection!).ID);
|
||||
AddStep("record selection", () => selectedID = Carousel.CurrentBeatmap!.ID);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ApplyToFilterAndWaitForFilter("filter all", c => c.SearchText = Guid.NewGuid().ToString());
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID);
|
||||
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
|
||||
AddAssert("selection not changed", () => ((BeatmapInfo)Carousel.CurrentSelection!).ID == selectedID);
|
||||
AddAssert("selection not changed", () => Carousel.CurrentBeatmap!.ID == selectedID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,14 +200,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
int diff = i;
|
||||
|
||||
AddStep($"select diff {diff}", () => Carousel.CurrentSelection = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
AddStep($"select diff {diff}", () => Carousel.CurrentBeatmap = chosenBeatmap = BeatmapSets[20].Beatmaps[diff]);
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Difficulty);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
AddAssert("selection retained", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap));
|
||||
|
||||
SortBy(SortMode.Title);
|
||||
AddAssert("selection retained", () => Carousel.CurrentSelection, () => Is.EqualTo(chosenBeatmap));
|
||||
AddAssert("selection retained", () => Carousel.CurrentBeatmap, () => Is.EqualTo(chosenBeatmap));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1;
|
||||
&& visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilterAndWaitForFilter("filter to taiko", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(1));
|
||||
@@ -249,8 +249,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 1) == 1;
|
||||
&& visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 1) == 1;
|
||||
});
|
||||
|
||||
ApplyToFilterAndWaitForFilter("filter to catch", c => c.Ruleset = rulesets.AvailableRulesets.ElementAt(2));
|
||||
@@ -260,8 +260,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
var visibleBeatmapPanels = GetVisiblePanels<PanelBeatmap>();
|
||||
|
||||
return visibleBeatmapPanels.Count() == 2
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((BeatmapInfo)p.Item!.Model).Ruleset.OnlineID == 2) == 1;
|
||||
&& visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 0) == 1
|
||||
&& visibleBeatmapPanels.Count(p => ((GroupedBeatmap)p.Item!.Model).Beatmap.Ruleset.OnlineID == 2) == 1;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
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;
|
||||
using osuTK;
|
||||
@@ -90,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
@@ -101,9 +100,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", GetSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((GroupedBeatmap)selection!).Beatmap.BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentGroupedBeatmap));
|
||||
AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
}
|
||||
@@ -390,15 +389,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
private void checkSelectionIterating(bool isIterating)
|
||||
{
|
||||
object? selection = null;
|
||||
GroupedBeatmap? selection = null;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
AddStep("store selection", () => selection = Carousel.CurrentSelection);
|
||||
AddStep("store selection", () => selection = Carousel.CurrentGroupedBeatmap);
|
||||
if (isIterating)
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection);
|
||||
AddUntilStep("selection changed", () => Carousel.CurrentGroupedBeatmap != selection);
|
||||
else
|
||||
AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection);
|
||||
AddUntilStep("selection not changed", () => Carousel.CurrentGroupedBeatmap == selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
nextRandom();
|
||||
ensureRandomDidNotRepeat();
|
||||
|
||||
AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!);
|
||||
AddStep("store selection", () => originalSelected = Carousel.CurrentBeatmap!);
|
||||
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Difficulty);
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected));
|
||||
AddAssert("selection not changed", () => Carousel.CurrentBeatmap, () => Is.EqualTo(originalSelected));
|
||||
|
||||
storeExpandedGroup();
|
||||
|
||||
@@ -247,18 +247,42 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindOverGroupingModeChange()
|
||||
{
|
||||
const int local_set_count = 3;
|
||||
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
AddBeatmaps(local_set_count, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextSet();
|
||||
|
||||
for (int i = 0; i < local_set_count; i++)
|
||||
nextRandom();
|
||||
|
||||
SortAndGroupBy(SortMode.Title, GroupMode.LastPlayed);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
for (int i = 0; i < local_set_count; i++)
|
||||
{
|
||||
prevRandomSet();
|
||||
checkRewindCorrectSet();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomThenRewindSameFrame()
|
||||
{
|
||||
AddBeatmaps(10, 3, true);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
BeatmapInfo? originalSelected = null;
|
||||
GroupedBeatmap? originalSelected = null;
|
||||
|
||||
nextRandom();
|
||||
|
||||
CheckHasSelection();
|
||||
AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!);
|
||||
AddStep("store selection", () => originalSelected = Carousel.CurrentGroupedBeatmap!);
|
||||
|
||||
AddStep("random then rewind", () =>
|
||||
{
|
||||
@@ -266,7 +290,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
Carousel.PreviousRandom();
|
||||
});
|
||||
|
||||
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(originalSelected));
|
||||
AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(originalSelected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -275,26 +299,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddBeatmaps(10, 3, true);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
BeatmapInfo? originalSelected = null;
|
||||
BeatmapInfo? postRandomSelection = null;
|
||||
GroupedBeatmap? originalSelected = null;
|
||||
GroupedBeatmap? postRandomSelection = null;
|
||||
|
||||
nextRandom();
|
||||
|
||||
CheckHasSelection();
|
||||
AddStep("store selection", () => originalSelected = (BeatmapInfo)Carousel.CurrentSelection!);
|
||||
AddStep("store selection", () => originalSelected = Carousel.CurrentGroupedBeatmap!);
|
||||
|
||||
nextRandom();
|
||||
AddStep("store selection", () => postRandomSelection = (BeatmapInfo)Carousel.CurrentSelection!);
|
||||
AddStep("store selection", () => postRandomSelection = Carousel.CurrentGroupedBeatmap!);
|
||||
|
||||
AddAssert("selection changed", () => originalSelected, () => Is.Not.SameAs(postRandomSelection));
|
||||
|
||||
AddStep("delete previous selection beatmaps", () => BeatmapSets.Remove(originalSelected!.BeatmapSet!));
|
||||
AddStep("delete previous selection beatmaps", () => BeatmapSets.Remove(originalSelected!.Beatmap.BeatmapSet!));
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection));
|
||||
AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(postRandomSelection));
|
||||
|
||||
prevRandomSet();
|
||||
AddAssert("selection not changed", () => Carousel.CurrentSelection, () => Is.EqualTo(postRandomSelection));
|
||||
AddAssert("selection not changed", () => Carousel.CurrentGroupedBeatmap, () => Is.EqualTo(postRandomSelection));
|
||||
}
|
||||
|
||||
private void nextRandom() =>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First());
|
||||
AddStep("select middle beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()));
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("override scroll with user scroll", () =>
|
||||
@@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("scroll to end", () => Scroll.ScrollToEnd(false));
|
||||
|
||||
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last().Beatmaps.Last());
|
||||
AddStep("select last beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.Last().Beatmaps.Last()));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||
AddStep("select first beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First()));
|
||||
|
||||
WaitForScrolling();
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
Quad positionBefore = default;
|
||||
|
||||
AddStep("select first beatmap", () => Carousel.CurrentSelection = BeatmapSets.First().Beatmaps.First());
|
||||
AddStep("select first beatmap", () => Carousel.CurrentGroupedBeatmap = new GroupedBeatmap(null, BeatmapSets.First().Beatmaps.First()));
|
||||
WaitForScrolling();
|
||||
|
||||
AddStep("override scroll with user scroll", () =>
|
||||
|
||||
@@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextSet();
|
||||
|
||||
WaitForSetSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap(b =>
|
||||
{
|
||||
@@ -195,8 +195,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
}
|
||||
|
||||
[Test] // Checks that we keep selection based on online ID where possible.
|
||||
@@ -205,15 +205,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextSet();
|
||||
|
||||
WaitForSetSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap(b => b.DifficultyName = "new name");
|
||||
assertDidFilter();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
}
|
||||
|
||||
[Test] // Checks that we fallback to keeping selection based on difficulty name.
|
||||
@@ -222,15 +222,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextSet();
|
||||
|
||||
WaitForSetSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
updateBeatmap(b => b.OnlineID = b.OnlineID + 1);
|
||||
assertDidFilter();
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
}
|
||||
|
||||
[Test] // Checks that we don't crash if there exists a difficulty with the same online ID as the selected difficulty.
|
||||
@@ -239,8 +239,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextSet();
|
||||
|
||||
WaitForSetSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
// Add another difficulty with same online ID.
|
||||
updateBeatmap(null, bs =>
|
||||
@@ -252,8 +252,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
}
|
||||
|
||||
[Test] // Checks that we don't crash if there exists a difficulty with the same name as the selected difficulty.
|
||||
@@ -262,8 +262,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectNextSet();
|
||||
|
||||
WaitForSetSelection(1, 0);
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
|
||||
// Remove original selected difficulty, and add two difficulties with same name as selection.
|
||||
updateBeatmap(null, bs =>
|
||||
@@ -284,8 +284,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
WaitForFiltering();
|
||||
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentSelection, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -104,21 +104,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap)
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap))
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
Selected = { Value = true }
|
||||
},
|
||||
new PanelBeatmap
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
KeyboardSelected = { Value = true },
|
||||
Selected = { Value = true }
|
||||
},
|
||||
|
||||
@@ -104,21 +104,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap)
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap))
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
KeyboardSelected = { Value = true }
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
Selected = { Value = true }
|
||||
},
|
||||
new PanelBeatmapStandalone
|
||||
{
|
||||
Item = new CarouselItem(beatmap),
|
||||
Item = new CarouselItem(new GroupedBeatmap(null, beatmap)),
|
||||
KeyboardSelected = { Value = true },
|
||||
Selected = { Value = true }
|
||||
},
|
||||
|
||||
@@ -320,9 +320,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
|
||||
AddUntilStep("wait for fail", () => ((Player)Stack.CurrentScreen).GameplayState.HasFailed);
|
||||
|
||||
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
|
||||
AddStep("exit gameplay", () => InputManager.Key(Key.Escape));
|
||||
AddStep("exit gameplay", () => Stack.CurrentScreen.Exit());
|
||||
|
||||
AddUntilStep("wait for song select", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect);
|
||||
AddUntilStep("wait for filtered", () => SongSelect.ChildrenOfType<BeatmapCarousel>().Single().FilterCount, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
/// </summary>
|
||||
public partial class TestSceneSongSelectCurrentSelectionInvalidated : SongSelectTestScene
|
||||
{
|
||||
private BeatmapInfo? selectedBeatmap => (BeatmapInfo?)Carousel.CurrentSelection;
|
||||
private BeatmapInfo? selectedBeatmap => Carousel.CurrentBeatmap;
|
||||
private BeatmapSetInfo? selectedBeatmapSet => selectedBeatmap?.BeatmapSet;
|
||||
|
||||
[SetUpSteps]
|
||||
|
||||
@@ -317,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddAssert($"\"{name}\" present", () =>
|
||||
{
|
||||
var group = grouping.GroupItems.Single(g => g.Key.Title == name);
|
||||
var actualBeatmaps = group.Value.Select(i => i.Model).OfType<BeatmapInfo>().OrderBy(b => b.ID);
|
||||
var actualBeatmaps = group.Value.Select(i => i.Model).OfType<GroupedBeatmap>().Select(gb => gb.Beatmap).OrderBy(b => b.ID);
|
||||
var expectedBeatmaps = getBeatmaps().SelectMany(s => s.Beatmaps).OrderBy(b => b.ID);
|
||||
return actualBeatmaps.SequenceEqual(expectedBeatmaps);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
@@ -341,27 +342,10 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
// Matches stable implementation, because it's probably simpler than trying to do anything else.
|
||||
// This may need to be reconsidered after we begin storing storyboards in the new editor.
|
||||
return windowsFilenameStrip(
|
||||
(metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile))
|
||||
+ (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty)
|
||||
+ @".osb");
|
||||
|
||||
string windowsFilenameStrip(string entry)
|
||||
{
|
||||
// Inlined from Path.GetInvalidFilenameChars() to ensure the windows characters are used (to match stable).
|
||||
char[] invalidCharacters =
|
||||
{
|
||||
'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
|
||||
'\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
|
||||
'\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
|
||||
'\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
foreach (char c in invalidCharacters)
|
||||
entry = entry.Replace(c.ToString(), string.Empty);
|
||||
|
||||
return entry;
|
||||
}
|
||||
string baseFilename = (metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile))
|
||||
+ (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty)
|
||||
+ @".osb";
|
||||
return baseFilename.GetValidFilename();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,16 @@ namespace osu.Game.Database
|
||||
foreach (var realmFile in model.Files)
|
||||
{
|
||||
string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath());
|
||||
string destinationPath = Path.Join(mountedPath, realmFile.Filename);
|
||||
// there are edge cases where externalising an imported model to the filesystem could fail due to invalid filenames.
|
||||
// one scenario where this happens goes something like this:
|
||||
// - stable user exports an archive, which contains filenames that get mangled by stable's default zip encoding codepage (Shift-JIS)
|
||||
// - said archive is imported to lazer, but the invalid filename is not actually an issue due to lazer file store structure
|
||||
// (the file is stored under a filename correspondent to its SHA instead, and its real filename is only stored in realm)
|
||||
// - however attempts to externally edit the model fail as the external edit attempts and fails to produce the file's "real" filename in the mounted path
|
||||
// to prevent this bricking external edit, strip invalid characters on external edit.
|
||||
// the presumption here is that whatever produced the mangled archive is primarily at fault here, and we're just trying to trudge on locally as best as possible.
|
||||
// if there are further troubles related to similar issues, reevaluate moving this sort of check to the import side instead (sanitising filenames on import from archive).
|
||||
string destinationPath = Path.Join(mountedPath, realmFile.Filename.GetValidFilename());
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ namespace osu.Game.Extensions
|
||||
/// DO NOT CHANGE THE SEMANTICS OF THIS METHOD unless you know well what you are doing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso href="https://github.com/peppy/osu-stable-reference/blob/67795dba3c308e7d0493b296149dcb073ca47ecb/osu!common/Helpers/GeneralHelper.cs#L41-L46"/>
|
||||
public static string GetValidFilename(this string filename)
|
||||
{
|
||||
foreach (char c in invalid_filename_chars)
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// The selection is never reset due to not existing. It can be set to anything.
|
||||
/// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches.
|
||||
/// </remarks>
|
||||
public object? CurrentSelection
|
||||
protected object? CurrentSelection
|
||||
{
|
||||
get => currentSelection.Model;
|
||||
set
|
||||
|
||||
@@ -41,12 +41,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// <summary>
|
||||
/// From the provided beatmaps, select the most appropriate one for the user's skill.
|
||||
/// </summary>
|
||||
public required Action<IEnumerable<BeatmapInfo>> RequestRecommendedSelection { private get; init; }
|
||||
public required Action<IEnumerable<GroupedBeatmap>> RequestRecommendedSelection { private get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Selection requested for the provided beatmap.
|
||||
/// </summary>
|
||||
public required Action<BeatmapInfo> RequestSelection { private get; init; }
|
||||
public required Action<GroupedBeatmap> RequestSelection { private get; init; }
|
||||
|
||||
public const float SPACING = 3f;
|
||||
|
||||
@@ -74,11 +74,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
return SPACING * 2;
|
||||
|
||||
// ..and the bottom.
|
||||
if (top.Model is BeatmapInfo && bottom.Model is GroupedBeatmapSet)
|
||||
if (top.Model is GroupedBeatmap && bottom.Model is GroupedBeatmapSet)
|
||||
return SPACING * 2;
|
||||
|
||||
// Beatmap difficulty panels do not overlap with themselves or any other panel.
|
||||
if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo)
|
||||
if (top.Model is GroupedBeatmap || bottom.Model is GroupedBeatmap)
|
||||
return SPACING;
|
||||
}
|
||||
else
|
||||
@@ -103,14 +103,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
new BeatmapCarouselFilterMatching(() => Criteria!),
|
||||
new BeatmapCarouselFilterSorting(() => Criteria!),
|
||||
grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, getDetachedCollections, getTopRanksMapping)
|
||||
grouping = new BeatmapCarouselFilterGrouping(() => Criteria!, GetAllCollections, GetBeatmapInfoGuidToTopRankMapping)
|
||||
};
|
||||
|
||||
AddInternal(loading = new LoadingLayer());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapStore beatmapStore, RealmAccess realm, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken)
|
||||
private void load(BeatmapStore beatmapStore, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken)
|
||||
{
|
||||
setupPools();
|
||||
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
|
||||
@@ -158,7 +158,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
foreach (var beatmap in set.Beatmaps)
|
||||
{
|
||||
Items.RemoveAll(i => i is BeatmapInfo bi && beatmap.Equals(bi));
|
||||
selectedSetDeleted |= CheckModelEquality(CurrentSelection, beatmap);
|
||||
selectedSetDeleted |= CheckModelEquality((CurrentSelection as GroupedBeatmap)?.Beatmap, beatmap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,13 +200,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
if (CheckValidForSetSelection(item))
|
||||
{
|
||||
if (item.Model is BeatmapInfo beatmapInfo)
|
||||
if (item.Model is GroupedBeatmap groupedBeatmap)
|
||||
{
|
||||
// check the new selection wasn't deleted above
|
||||
if (!Items.Contains(beatmapInfo))
|
||||
if (!Items.Contains(groupedBeatmap.Beatmap))
|
||||
return false;
|
||||
|
||||
RequestSelection(beatmapInfo);
|
||||
RequestSelection(groupedBeatmap);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (oldItems.Contains(groupedSet.BeatmapSet))
|
||||
return false;
|
||||
|
||||
RequestRecommendedSelection(groupedSet.BeatmapSet.Beatmaps);
|
||||
selectRecommendedDifficultyForBeatmapSet(groupedSet);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -256,8 +256,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
// TODO: should this exist in song select instead of here?
|
||||
// we need to ensure the global beatmap is also updated alongside changes.
|
||||
if (CurrentSelection != null && CheckModelEquality(beatmap, CurrentSelection))
|
||||
RequestSelection(matchingNewBeatmap);
|
||||
if (CurrentSelection is GroupedBeatmap currentBeatmapUnderGrouping)
|
||||
{
|
||||
var candidateSelection = currentBeatmapUnderGrouping with { Beatmap = beatmap };
|
||||
if (CheckModelEquality(candidateSelection, CurrentSelection))
|
||||
RequestSelection(candidateSelection);
|
||||
}
|
||||
|
||||
Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]);
|
||||
newSetBeatmaps.Remove(matchingNewBeatmap);
|
||||
@@ -289,7 +293,51 @@ namespace osu.Game.Screens.SelectV2
|
||||
protected GroupedBeatmapSet? ExpandedBeatmapSet { get; private set; }
|
||||
|
||||
protected override bool ShouldActivateOnKeyboardSelection(CarouselItem item) =>
|
||||
grouping.BeatmapSetsGroupedTogether && item.Model is BeatmapInfo;
|
||||
grouping.BeatmapSetsGroupedTogether && item.Model is GroupedBeatmap;
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected <see cref="GroupedBeatmap"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The selection is never reset due to not existing. It can be set to anything.
|
||||
/// If no matching carousel item exists, there will be no visually selected item while waiting for potential new item which matches.
|
||||
/// </remarks>
|
||||
public GroupedBeatmap? CurrentGroupedBeatmap
|
||||
{
|
||||
get => CurrentSelection as GroupedBeatmap;
|
||||
set => CurrentSelection = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a property mostly dedicated to external consumers who only care about showing some particular copy of a beatmap
|
||||
/// (there could be multiple panels for one beatmap due to grouping).
|
||||
/// Through this property, the carousel basically figures out what group to use internally.
|
||||
/// </remarks>
|
||||
public BeatmapInfo? CurrentBeatmap
|
||||
{
|
||||
get => CurrentGroupedBeatmap?.Beatmap;
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
CurrentGroupedBeatmap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentGroupedBeatmap != null && value.Equals(CurrentGroupedBeatmap.Beatmap))
|
||||
return;
|
||||
|
||||
// it is not universally guaranteed that the carousel items will be materialised at the time this is set.
|
||||
// therefore, in cases where it is known that they will not be, default to a null group.
|
||||
// even if grouping is active, this will be rectified to a correct group on the next invocation of `HandleFilterCompleted()`.
|
||||
CurrentGroupedBeatmap = IsLoaded && !IsFiltering
|
||||
? GetCarouselItems()?.Select(item => item.Model).OfType<GroupedBeatmap>().FirstOrDefault(gb => gb.Beatmap.Equals(value))
|
||||
: new GroupedBeatmap(null, value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleItemActivated(CarouselItem item)
|
||||
{
|
||||
@@ -309,8 +357,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
setExpandedGroup(group);
|
||||
|
||||
// If the active selection is within this group, it should get keyboard focus immediately.
|
||||
if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is BeatmapInfo info)
|
||||
RequestSelection(info);
|
||||
if (CurrentSelectionItem?.IsVisible == true && CurrentSelection is GroupedBeatmap gb)
|
||||
RequestSelection(gb);
|
||||
|
||||
return;
|
||||
|
||||
@@ -318,14 +366,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
selectRecommendedDifficultyForBeatmapSet(groupedSet);
|
||||
return;
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
if (CurrentSelection != null && CheckModelEquality(CurrentSelection, beatmapInfo))
|
||||
case GroupedBeatmap groupedBeatmap:
|
||||
if (CurrentSelection != null && CheckModelEquality(CurrentSelection, groupedBeatmap))
|
||||
{
|
||||
RequestPresentBeatmap?.Invoke(beatmapInfo);
|
||||
RequestPresentBeatmap?.Invoke(groupedBeatmap.Beatmap);
|
||||
return;
|
||||
}
|
||||
|
||||
RequestSelection(beatmapInfo);
|
||||
RequestSelection(groupedBeatmap);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -345,14 +393,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupDefinition:
|
||||
throw new InvalidOperationException("Groups should never become selected");
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
// Find any containing group. There should never be too many groups so iterating is efficient enough.
|
||||
GroupDefinition? containingGroup = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => CheckModelEquality(i.Model, beatmapInfo))).Key;
|
||||
|
||||
setExpandedGroup(containingGroup);
|
||||
case GroupedBeatmap groupedBeatmap:
|
||||
setExpandedGroup(groupedBeatmap.Group);
|
||||
|
||||
if (grouping.BeatmapSetsGroupedTogether)
|
||||
setExpandedSet(new GroupedBeatmapSet(containingGroup, beatmapInfo.BeatmapSet!));
|
||||
setExpandedSet(new GroupedBeatmapSet(groupedBeatmap.Group, groupedBeatmap.Beatmap.BeatmapSet!));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -417,9 +462,27 @@ namespace osu.Game.Screens.SelectV2
|
||||
// Store selected group before handling selection (it may implicitly change the expanded group).
|
||||
var groupForReselection = ExpandedGroup;
|
||||
|
||||
// Ensure correct post-selection logic is handled on the new items list.
|
||||
// This will update the visual state of the selected item.
|
||||
HandleItemSelected(CurrentSelection);
|
||||
var currentGroupedBeatmap = CurrentSelection as GroupedBeatmap;
|
||||
|
||||
// The filter might have changed the set of available groups, which means that the current selection may point to a stale group.
|
||||
// Check whether that is the case.
|
||||
bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0;
|
||||
bool groupStillExists = currentGroupedBeatmap?.Group != null && grouping.GroupItems.ContainsKey(currentGroupedBeatmap.Group);
|
||||
|
||||
if (groupingRemainsOff || groupStillExists)
|
||||
{
|
||||
// Only update the visual state of the selected item.
|
||||
HandleItemSelected(currentGroupedBeatmap);
|
||||
}
|
||||
else if (currentGroupedBeatmap != null)
|
||||
{
|
||||
// If the group no longer exists, grab an arbitrary other instance of the beatmap under the first group encountered.
|
||||
var newSelection = GetCarouselItems()?.Select(i => i.Model).OfType<GroupedBeatmap>().FirstOrDefault(gb => gb.Beatmap.Equals(currentGroupedBeatmap.Beatmap));
|
||||
// Only change the selection if we actually got a positive hit.
|
||||
// This is necessary so that selection isn't lost if the panel reappears later due to e.g. unapplying some filter criteria that made it disappear in the first place.
|
||||
if (newSelection != null)
|
||||
CurrentSelection = newSelection;
|
||||
}
|
||||
|
||||
// If a group was selected that is not the one containing the selection, attempt to reselect it.
|
||||
// If the original group was not found, ExpandedGroup will already have been updated to a valid value in `HandleItemSelected` above.
|
||||
@@ -432,7 +495,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
// Selecting a set isn't valid – let's re-select the first visible difficulty.
|
||||
if (grouping.SetItems.TryGetValue(set, out var items))
|
||||
{
|
||||
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
|
||||
var beatmaps = items.Select(i => i.Model).OfType<GroupedBeatmap>();
|
||||
RequestRecommendedSelection(beatmaps);
|
||||
}
|
||||
}
|
||||
@@ -450,8 +513,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Model is BeatmapInfo beatmapInfo)
|
||||
if (item.Model is GroupedBeatmap groupedBeatmap)
|
||||
{
|
||||
var beatmapInfo = groupedBeatmap.Beatmap;
|
||||
|
||||
if (beatmapSetInfo == null)
|
||||
{
|
||||
beatmapSetInfo = beatmapInfo.BeatmapSet!;
|
||||
@@ -464,9 +529,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
var beatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
|
||||
var beatmaps = items.Select(i => i.Model).OfType<GroupedBeatmap>();
|
||||
|
||||
if (beatmaps.Any(b => b.Equals(CurrentSelection as BeatmapInfo)))
|
||||
if (beatmaps.Any(b => b.Equals(CurrentSelection as GroupedBeatmap)))
|
||||
return;
|
||||
|
||||
RequestRecommendedSelection(beatmaps);
|
||||
@@ -481,7 +546,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupedBeatmapSet:
|
||||
return true;
|
||||
|
||||
case BeatmapInfo:
|
||||
case GroupedBeatmap:
|
||||
return !grouping.BeatmapSetsGroupedTogether;
|
||||
|
||||
case GroupDefinition:
|
||||
@@ -492,7 +557,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedGroup(GroupDefinition group)
|
||||
private void setExpandedGroup(GroupDefinition? group)
|
||||
{
|
||||
if (ExpandedGroup != null)
|
||||
setExpansionStateOfGroup(ExpandedGroup, false);
|
||||
@@ -500,7 +565,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
ExpandedGroup = group;
|
||||
|
||||
if (ExpandedGroup != null)
|
||||
setExpansionStateOfGroup(group, true);
|
||||
setExpansionStateOfGroup(ExpandedGroup, true);
|
||||
}
|
||||
|
||||
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
|
||||
@@ -607,7 +672,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
sampleChangeSet?.Play();
|
||||
return;
|
||||
|
||||
case BeatmapInfo:
|
||||
case GroupedBeatmap:
|
||||
sampleChangeDifficulty?.Play();
|
||||
return;
|
||||
}
|
||||
@@ -687,9 +752,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private List<BeatmapCollection> getDetachedCollections() => realm.Run(r => r.All<BeatmapCollection>().AsEnumerable().Detach());
|
||||
protected virtual List<BeatmapCollection> GetAllCollections() => realm.Run(r => r.All<BeatmapCollection>().AsEnumerable().Detach());
|
||||
|
||||
private Dictionary<Guid, ScoreRank> getTopRanksMapping(FilterCriteria criteria) => realm.Run(r =>
|
||||
protected virtual Dictionary<Guid, ScoreRank> GetBeatmapInfoGuidToTopRankMapping(FilterCriteria criteria) => realm.Run(r =>
|
||||
{
|
||||
var topRankMapping = new Dictionary<Guid, ScoreRank>();
|
||||
|
||||
@@ -745,8 +810,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
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);
|
||||
if (x is GroupedBeatmap groupedBeatmapX && y is GroupedBeatmap groupedBeatmapY)
|
||||
return groupedBeatmapX.Equals(groupedBeatmapY);
|
||||
|
||||
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
|
||||
return groupX.Equals(groupY);
|
||||
@@ -767,7 +832,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupDefinition:
|
||||
return groupPanelPool.Get();
|
||||
|
||||
case BeatmapInfo:
|
||||
case GroupedBeatmap:
|
||||
if (!grouping.BeatmapSetsGroupedTogether)
|
||||
return standalonePanelPool.Get();
|
||||
|
||||
@@ -785,8 +850,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
#region Random selection handling
|
||||
|
||||
private readonly Bindable<RandomSelectAlgorithm> randomAlgorithm = new Bindable<RandomSelectAlgorithm>();
|
||||
private readonly List<BeatmapInfo> previouslyVisitedRandomBeatmaps = new List<BeatmapInfo>();
|
||||
private readonly List<BeatmapInfo> randomHistory = new List<BeatmapInfo>();
|
||||
private readonly HashSet<BeatmapInfo> previouslyVisitedRandomBeatmaps = new HashSet<BeatmapInfo>();
|
||||
private readonly List<GroupedBeatmap> randomHistory = new List<GroupedBeatmap>();
|
||||
|
||||
private Sample? spinSample;
|
||||
private Sample? randomSelectSample;
|
||||
@@ -799,7 +864,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
return false;
|
||||
|
||||
var selectionBefore = CurrentSelectionItem;
|
||||
var beatmapBefore = selectionBefore?.Model as BeatmapInfo;
|
||||
var beatmapBefore = selectionBefore?.Model as GroupedBeatmap;
|
||||
|
||||
bool success;
|
||||
|
||||
@@ -809,7 +874,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
randomHistory.Add(beatmapBefore);
|
||||
// keep track of visited beatmaps for "RandomPermutation" random tracking.
|
||||
// note that this is reset when we run out of beatmaps, while `randomHistory` is not.
|
||||
previouslyVisitedRandomBeatmaps.Add(beatmapBefore);
|
||||
previouslyVisitedRandomBeatmaps.Add(beatmapBefore.Beatmap);
|
||||
}
|
||||
|
||||
if (grouping.BeatmapSetsGroupedTogether)
|
||||
@@ -837,29 +902,29 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private bool nextRandomBeatmap()
|
||||
{
|
||||
ICollection<BeatmapInfo> visibleBeatmaps = ExpandedGroup != null
|
||||
ICollection<GroupedBeatmap> visibleBeatmaps = 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<BeatmapInfo>().ToArray()
|
||||
: GetCarouselItems()!.Select(i => i.Model).OfType<BeatmapInfo>().ToArray();
|
||||
? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType<GroupedBeatmap>().ToArray()
|
||||
: GetCarouselItems()!.Select(i => i.Model).OfType<GroupedBeatmap>().ToArray();
|
||||
|
||||
BeatmapInfo beatmap;
|
||||
GroupedBeatmap beatmap;
|
||||
|
||||
switch (randomAlgorithm.Value)
|
||||
{
|
||||
case RandomSelectAlgorithm.RandomPermutation:
|
||||
{
|
||||
ICollection<BeatmapInfo> notYetVisitedBeatmaps = visibleBeatmaps.Except(previouslyVisitedRandomBeatmaps).ToList();
|
||||
ICollection<GroupedBeatmap> notYetVisitedBeatmaps = visibleBeatmaps.ExceptBy(previouslyVisitedRandomBeatmaps, gb => gb.Beatmap).ToList();
|
||||
|
||||
if (!notYetVisitedBeatmaps.Any())
|
||||
{
|
||||
previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleBeatmaps.Contains(b));
|
||||
previouslyVisitedRandomBeatmaps.ExceptWith(visibleBeatmaps.Select(b => b.Beatmap));
|
||||
notYetVisitedBeatmaps = visibleBeatmaps;
|
||||
if (CurrentSelection is BeatmapInfo beatmapInfo)
|
||||
notYetVisitedBeatmaps = notYetVisitedBeatmaps.Except([beatmapInfo]).ToList();
|
||||
if (CurrentSelection is GroupedBeatmap groupedBeatmap)
|
||||
notYetVisitedBeatmaps = notYetVisitedBeatmaps.Except([groupedBeatmap]).ToList();
|
||||
}
|
||||
|
||||
if (notYetVisitedBeatmaps.Count == 0)
|
||||
@@ -883,7 +948,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private bool nextRandomSet()
|
||||
{
|
||||
ICollection<GroupedBeatmapSet> visibleSetsUnderGrouping = ExpandedGroup != null
|
||||
ICollection<GroupedBeatmapSet> visibleGroupedSets = 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.
|
||||
//
|
||||
@@ -900,14 +965,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
case RandomSelectAlgorithm.RandomPermutation:
|
||||
{
|
||||
ICollection<GroupedBeatmapSet> notYetVisitedSets =
|
||||
visibleSetsUnderGrouping.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList();
|
||||
visibleGroupedSets.ExceptBy(previouslyVisitedRandomBeatmaps.Select(b => b.BeatmapSet!), groupedSet => groupedSet.BeatmapSet).ToList();
|
||||
|
||||
if (!notYetVisitedSets.Any())
|
||||
{
|
||||
previouslyVisitedRandomBeatmaps.RemoveAll(b => visibleSetsUnderGrouping.Any(groupedSet => groupedSet.BeatmapSet.Equals(b.BeatmapSet!)));
|
||||
notYetVisitedSets = visibleSetsUnderGrouping;
|
||||
if (CurrentSelection is BeatmapInfo beatmapInfo)
|
||||
notYetVisitedSets = notYetVisitedSets.ExceptBy([beatmapInfo.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList();
|
||||
previouslyVisitedRandomBeatmaps.ExceptWith(visibleGroupedSets.SelectMany(setUnderGrouping => setUnderGrouping.BeatmapSet.Beatmaps));
|
||||
notYetVisitedSets = visibleGroupedSets;
|
||||
if (CurrentSelection is GroupedBeatmap groupedBeatmap)
|
||||
notYetVisitedSets = notYetVisitedSets.ExceptBy([groupedBeatmap.Beatmap.BeatmapSet!], groupedSet => groupedSet.BeatmapSet).ToList();
|
||||
}
|
||||
|
||||
if (notYetVisitedSets.Count == 0)
|
||||
@@ -918,7 +983,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
|
||||
case RandomSelectAlgorithm.Random:
|
||||
set = visibleSetsUnderGrouping.ElementAt(RNG.Next(visibleSetsUnderGrouping.Count));
|
||||
set = visibleGroupedSets.ElementAt(RNG.Next(visibleGroupedSets.Count));
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -941,15 +1006,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
var previousBeatmap = randomHistory[^1];
|
||||
randomHistory.RemoveAt(randomHistory.Count - 1);
|
||||
|
||||
var previousBeatmapItem = carouselItems.FirstOrDefault(i => i.Model is BeatmapInfo b && b.Equals(previousBeatmap));
|
||||
// when going back through rewind history, we may no longer be in the same grouping mode.
|
||||
// the user wants to go back to the beatmap first and foremost, so the most important thing is to find a panel that corresponds to the beatmap.
|
||||
// going back to the same group is a nice-to-have, but a secondary concern.
|
||||
var previousBeatmapItem = carouselItems.Where(i => i.Model is GroupedBeatmap gb && gb.Beatmap.Equals(previousBeatmap.Beatmap))
|
||||
.MaxBy(i => ((GroupedBeatmap)i.Model).Group == previousBeatmap.Group);
|
||||
|
||||
if (previousBeatmapItem == null)
|
||||
return false;
|
||||
|
||||
if (CurrentSelection is BeatmapInfo beatmapInfo)
|
||||
if (CurrentSelection is GroupedBeatmap groupedBeatmap)
|
||||
{
|
||||
if (randomAlgorithm.Value == RandomSelectAlgorithm.RandomPermutation)
|
||||
previouslyVisitedRandomBeatmaps.Remove(beatmapInfo);
|
||||
previouslyVisitedRandomBeatmaps.Remove(groupedBeatmap.Beatmap);
|
||||
|
||||
if (CurrentSelectionItem == null)
|
||||
playSpinSample(0);
|
||||
@@ -957,7 +1026,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
playSpinSample(visiblePanelCountBetweenItems(previousBeatmapItem, CurrentSelectionItem));
|
||||
}
|
||||
|
||||
RequestSelection(previousBeatmap);
|
||||
RequestSelection((GroupedBeatmap)previousBeatmapItem.Model);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1021,4 +1090,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it.
|
||||
/// </summary>
|
||||
public record GroupedBeatmapSet([UsedImplicitly] GroupDefinition? Group, BeatmapSetInfo BeatmapSet);
|
||||
|
||||
/// <summary>
|
||||
/// Used to represent a <see cref="Beatmap"/> under a <see cref="GroupDefinition"/>.
|
||||
/// The purpose of this model is to support showing multiple copies of a beatmap, which can occur if a beatmap appears in multiple groups
|
||||
/// (most prominently, collections group mode).
|
||||
/// </summary>
|
||||
public record GroupedBeatmap(GroupDefinition? Group, BeatmapInfo Beatmap);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
@@ -120,11 +121,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
if (groupItem != null)
|
||||
groupItem.NestedItemCount++;
|
||||
|
||||
item.DrawHeight = PanelBeatmapStandalone.HEIGHT;
|
||||
}
|
||||
|
||||
addItem(item);
|
||||
addItem(new CarouselItem(new GroupedBeatmap(group, beatmap))
|
||||
{
|
||||
DrawHeight = BeatmapSetsGroupedTogether ? PanelBeatmap.HEIGHT : PanelBeatmapStandalone.HEIGHT,
|
||||
});
|
||||
lastBeatmap = beatmap;
|
||||
displayedBeatmapsCount++;
|
||||
}
|
||||
@@ -192,7 +194,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
var date = b.LastPlayed;
|
||||
|
||||
if (date == null || date == DateTimeOffset.MinValue)
|
||||
return new GroupDefinition(int.MaxValue, "Never");
|
||||
return new GroupDefinition(int.MaxValue, "Never").Yield();
|
||||
|
||||
return defineGroupByDate(date.Value);
|
||||
}, items);
|
||||
@@ -236,184 +238,204 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
private List<GroupMapping> getGroupsBy(Func<BeatmapInfo, GroupDefinition?> getGroup, List<CarouselItem> items)
|
||||
private List<GroupMapping> getGroupsBy(Func<BeatmapInfo, IEnumerable<GroupDefinition>> defineGroups, List<CarouselItem> items)
|
||||
{
|
||||
return items.GroupBy(i => getGroup((BeatmapInfo)i.Model))
|
||||
.Where(g => g.Key != null)
|
||||
.OrderBy(g => g.Key!.Order)
|
||||
.ThenBy(g => g.Key!.Title)
|
||||
.Select(g => new GroupMapping(g.Key, g.ToList()))
|
||||
.ToList();
|
||||
var groups = new Dictionary<GroupDefinition, GroupMapping>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
foreach (var groupDefinition in defineGroups((BeatmapInfo)item.Model))
|
||||
{
|
||||
if (!groups.TryGetValue(groupDefinition, out var group))
|
||||
group = groups[groupDefinition] = new GroupMapping(groupDefinition, []);
|
||||
|
||||
group.ItemsInGroup.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return groups.Values
|
||||
.OrderBy(g => g.Group!.Order)
|
||||
.ThenBy(g => g.Group!.Title)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupAlphabetically(string name)
|
||||
private IEnumerable<GroupDefinition> defineGroupAlphabetically(string name)
|
||||
{
|
||||
char firstChar = name.FirstOrDefault();
|
||||
|
||||
if (char.IsAsciiDigit(firstChar))
|
||||
return new GroupDefinition(int.MinValue, "0-9");
|
||||
return new GroupDefinition(int.MinValue, "0-9").Yield();
|
||||
|
||||
if (char.IsAsciiLetter(firstChar))
|
||||
return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString());
|
||||
return new GroupDefinition(char.ToUpperInvariant(firstChar) - 'A', char.ToUpperInvariant(firstChar).ToString()).Yield();
|
||||
|
||||
return new GroupDefinition(int.MaxValue, "Other");
|
||||
return new GroupDefinition(int.MaxValue, "Other").Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByDate(DateTimeOffset date)
|
||||
private IEnumerable<GroupDefinition> defineGroupByDate(DateTimeOffset date)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
var elapsed = now - date;
|
||||
|
||||
if (elapsed.TotalDays < 1)
|
||||
return new GroupDefinition(0, "Today");
|
||||
return new GroupDefinition(0, "Today").Yield();
|
||||
|
||||
if (elapsed.TotalDays < 2)
|
||||
return new GroupDefinition(1, "Yesterday");
|
||||
return new GroupDefinition(1, "Yesterday").Yield();
|
||||
|
||||
if (elapsed.TotalDays < 7)
|
||||
return new GroupDefinition(2, "Last week");
|
||||
return new GroupDefinition(2, "Last week").Yield();
|
||||
|
||||
if (elapsed.TotalDays < 30)
|
||||
return new GroupDefinition(3, "Last month");
|
||||
return new GroupDefinition(3, "Last month").Yield();
|
||||
|
||||
if (elapsed.TotalDays < 60)
|
||||
return new GroupDefinition(4, "1 month ago");
|
||||
return new GroupDefinition(4, "1 month ago").Yield();
|
||||
|
||||
for (int i = 90; i <= 150; i += 30)
|
||||
{
|
||||
if (elapsed.TotalDays < i)
|
||||
return new GroupDefinition(i, $"{i / 30 - 1} months ago");
|
||||
return new GroupDefinition(i, $"{i / 30 - 1} months ago").Yield();
|
||||
}
|
||||
|
||||
return new GroupDefinition(151, "Over 5 months ago");
|
||||
return new GroupDefinition(151, "Over 5 months ago").Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByRankedDate(DateTimeOffset? date)
|
||||
private IEnumerable<GroupDefinition> defineGroupByRankedDate(DateTimeOffset? date)
|
||||
{
|
||||
if (date == null)
|
||||
return new GroupDefinition(0, "Unranked");
|
||||
return new GroupDefinition(0, "Unranked").Yield();
|
||||
|
||||
return new GroupDefinition(-date.Value.Year, $"{date.Value.Year}");
|
||||
return new GroupDefinition(-date.Value.Year, $"{date.Value.Year}").Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByStatus(BeatmapOnlineStatus status)
|
||||
private IEnumerable<GroupDefinition> defineGroupByStatus(BeatmapOnlineStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case BeatmapOnlineStatus.Ranked:
|
||||
case BeatmapOnlineStatus.Approved:
|
||||
return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription());
|
||||
return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.Qualified:
|
||||
return new GroupDefinition(1, status.GetDescription());
|
||||
return new GroupDefinition(1, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.WIP:
|
||||
return new GroupDefinition(2, status.GetDescription());
|
||||
return new GroupDefinition(2, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.Pending:
|
||||
return new GroupDefinition(3, status.GetDescription());
|
||||
return new GroupDefinition(3, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.Graveyard:
|
||||
return new GroupDefinition(4, status.GetDescription());
|
||||
return new GroupDefinition(4, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
return new GroupDefinition(5, status.GetDescription());
|
||||
return new GroupDefinition(5, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.None:
|
||||
return new GroupDefinition(6, status.GetDescription());
|
||||
return new GroupDefinition(6, status.GetDescription()).Yield();
|
||||
|
||||
case BeatmapOnlineStatus.Loved:
|
||||
return new GroupDefinition(7, status.GetDescription());
|
||||
return new GroupDefinition(7, status.GetDescription()).Yield();
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(status), status, null);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByBPM(double bpm)
|
||||
private IEnumerable<GroupDefinition> defineGroupByBPM(double bpm)
|
||||
{
|
||||
if (bpm < 60)
|
||||
return new GroupDefinition(60, "Under 60 BPM");
|
||||
return new GroupDefinition(60, "Under 60 BPM").Yield();
|
||||
|
||||
for (int i = 70; i <= 300; i += 10)
|
||||
{
|
||||
if (bpm < i)
|
||||
return new GroupDefinition(i, $"{i - 10} - {i} BPM");
|
||||
return new GroupDefinition(i, $"{i - 10} - {i} BPM").Yield();
|
||||
}
|
||||
|
||||
return new GroupDefinition(301, "Over 300 BPM");
|
||||
return new GroupDefinition(301, "Over 300 BPM").Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByStars(double stars)
|
||||
private IEnumerable<GroupDefinition> defineGroupByStars(double stars)
|
||||
{
|
||||
// truncation is intentional - compare `FormatUtils.FormatStarRating()`
|
||||
int starInt = (int)stars;
|
||||
var starDifficulty = new StarDifficulty(starInt, 0);
|
||||
|
||||
if (starInt == 0)
|
||||
return new StarDifficultyGroupDefinition(0, "Below 1 Star", starDifficulty);
|
||||
return new StarDifficultyGroupDefinition(0, "Below 1 Star", starDifficulty).Yield();
|
||||
|
||||
if (starInt == 1)
|
||||
return new StarDifficultyGroupDefinition(1, "1 Star", starDifficulty);
|
||||
return new StarDifficultyGroupDefinition(1, "1 Star", starDifficulty).Yield();
|
||||
|
||||
return new StarDifficultyGroupDefinition(starInt, $"{starInt} Stars", starDifficulty);
|
||||
return new StarDifficultyGroupDefinition(starInt, $"{starInt} Stars", starDifficulty).Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByLength(double length)
|
||||
private IEnumerable<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(1, "1 minute or less").Yield();
|
||||
|
||||
return new GroupDefinition(i, $"{i} minutes or less");
|
||||
return new GroupDefinition(i, $"{i} minutes or less").Yield();
|
||||
}
|
||||
}
|
||||
|
||||
if (length <= 10 * 60_000)
|
||||
return new GroupDefinition(10, "10 minutes or less");
|
||||
return new GroupDefinition(10, "10 minutes or less").Yield();
|
||||
|
||||
return new GroupDefinition(11, "Over 10 minutes");
|
||||
return new GroupDefinition(11, "Over 10 minutes").Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupBySource(string source)
|
||||
private IEnumerable<GroupDefinition> defineGroupBySource(string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source))
|
||||
return new GroupDefinition(1, "Unsourced");
|
||||
return new GroupDefinition(1, "Unsourced").Yield();
|
||||
|
||||
return new GroupDefinition(0, source);
|
||||
return new GroupDefinition(0, source).Yield();
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByCollection(BeatmapInfo beatmap, IEnumerable<BeatmapCollection> collections)
|
||||
private IEnumerable<GroupDefinition> defineGroupByCollection(BeatmapInfo beatmap, IEnumerable<BeatmapCollection> collections)
|
||||
{
|
||||
bool anyCollections = false;
|
||||
|
||||
foreach (var collection in collections)
|
||||
{
|
||||
if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash))
|
||||
return new GroupDefinition(0, collection.Name);
|
||||
{
|
||||
yield return new GroupDefinition(0, collection.Name);
|
||||
|
||||
anyCollections = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new GroupDefinition(1, "Not in collection");
|
||||
if (anyCollections)
|
||||
yield break;
|
||||
|
||||
yield return new GroupDefinition(1, "Not in collection");
|
||||
}
|
||||
|
||||
private GroupDefinition? defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername)
|
||||
private IEnumerable<GroupDefinition> defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername)
|
||||
{
|
||||
var author = beatmap.BeatmapSet!.Metadata.Author;
|
||||
|
||||
if (author.OnlineID == localUserId || (author.OnlineID <= 1 && author.Username == localUserUsername))
|
||||
return new GroupDefinition(0, "My maps");
|
||||
return new GroupDefinition(0, "My maps").Yield();
|
||||
|
||||
// discard beatmaps not owned by the user.
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
private GroupDefinition defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary<Guid, ScoreRank> topRankMapping)
|
||||
private IEnumerable<GroupDefinition> defineGroupByRankAchieved(BeatmapInfo beatmap, IReadOnlyDictionary<Guid, ScoreRank> topRankMapping)
|
||||
{
|
||||
if (topRankMapping.TryGetValue(beatmap.ID, out var rank))
|
||||
return new GroupDefinition(-(int)rank, rank.GetDescription());
|
||||
return new GroupDefinition(-(int)rank, rank.GetDescription()).Yield();
|
||||
|
||||
return new GroupDefinition(int.MaxValue, "Unplayed");
|
||||
return new GroupDefinition(int.MaxValue, "Unplayed").Yield();
|
||||
}
|
||||
|
||||
private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -72,6 +71,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
[Resolved]
|
||||
private ISongSelect? songSelect { get; set; }
|
||||
|
||||
private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap;
|
||||
|
||||
public PanelBeatmap()
|
||||
{
|
||||
PanelXOffset = 60;
|
||||
@@ -207,9 +208,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
difficultyIcon.Icon = getRulesetIcon(beatmap.Ruleset);
|
||||
|
||||
localRank.Beatmap = beatmap;
|
||||
@@ -248,8 +246,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
|
||||
starDifficultyBindable.BindValueChanged(starDifficulty =>
|
||||
{
|
||||
@@ -293,8 +289,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
@@ -319,7 +313,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (songSelect != null)
|
||||
items.AddRange(songSelect.GetForwardActions((BeatmapInfo)Item.Model));
|
||||
items.AddRange(songSelect.GetForwardActions(beatmap));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -73,6 +72,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private Box backgroundBorder = null!;
|
||||
|
||||
private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap;
|
||||
|
||||
public PanelBeatmapStandalone()
|
||||
{
|
||||
PanelXOffset = 20;
|
||||
@@ -219,9 +220,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
var beatmapSet = beatmap.BeatmapSet!;
|
||||
|
||||
beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmap);
|
||||
@@ -262,8 +260,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
|
||||
starDifficultyBindable.BindValueChanged(starDifficulty =>
|
||||
{
|
||||
@@ -300,8 +296,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (Item == null)
|
||||
return;
|
||||
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
if (ruleset.Value.OnlineID == 3)
|
||||
{
|
||||
// Account for mania differences locally for now.
|
||||
@@ -326,7 +320,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (songSelect != null)
|
||||
items.AddRange(songSelect.GetForwardActions((BeatmapInfo)Item.Model));
|
||||
items.AddRange(songSelect.GetForwardActions(beatmap));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
@@ -300,9 +300,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
});
|
||||
}
|
||||
|
||||
private void requestRecommendedSelection(IEnumerable<BeatmapInfo> b)
|
||||
private void requestRecommendedSelection(IEnumerable<GroupedBeatmap> groupedBeatmaps)
|
||||
{
|
||||
queueBeatmapSelection(difficultyRecommender?.GetRecommendedBeatmap(b) ?? b.First());
|
||||
var recommendedBeatmap = difficultyRecommender?.GetRecommendedBeatmap(groupedBeatmaps.Select(gb => gb.Beatmap)) ?? groupedBeatmaps.First().Beatmap;
|
||||
queueBeatmapSelection(groupedBeatmaps.First(bug => bug.Beatmap.Equals(recommendedBeatmap)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -530,16 +531,16 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// - After <see cref="SELECTION_DEBOUNCE"/>, update the global beatmap. This in turn causes song select visuals (title, details, leaderboard) to update.
|
||||
/// This debounce is intended to avoid high overheads from churning lookups while a user is changing selection via rapid keyboard operations.
|
||||
/// </remarks>
|
||||
/// <param name="beatmap">The beatmap to be selected.</param>
|
||||
private void queueBeatmapSelection(BeatmapInfo beatmap)
|
||||
/// <param name="groupedBeatmap">The beatmap to be selected.</param>
|
||||
private void queueBeatmapSelection(GroupedBeatmap groupedBeatmap)
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
carousel.CurrentSelection = beatmap;
|
||||
carousel.CurrentGroupedBeatmap = groupedBeatmap;
|
||||
|
||||
// Debounce consideration is to avoid beatmap churn on key repeat selection.
|
||||
debounceQueueSelection(beatmap);
|
||||
debounceQueueSelection(groupedBeatmap.Beatmap);
|
||||
}
|
||||
|
||||
private bool ensureGlobalBeatmapValid()
|
||||
@@ -560,7 +561,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
if (validSelection)
|
||||
{
|
||||
carousel.CurrentSelection = currentBeatmap.BeatmapInfo;
|
||||
carousel.CurrentBeatmap = currentBeatmap.BeatmapInfo;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -583,7 +584,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
if (validBeatmaps.Any())
|
||||
{
|
||||
requestRecommendedSelection(validBeatmaps);
|
||||
carousel.CurrentBeatmap = difficultyRecommender?.GetRecommendedBeatmap(validBeatmaps) ?? validBeatmaps.First();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.829.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.903.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.821.0" />
|
||||
<PackageReference Include="Sentry" Version="5.1.1" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
||||
+1
-1
@@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.829.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.903.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user