1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-13 04:33:38 +08:00

Use song title as tie-breaker before date added when sorting song select (#36971)

<details>

<summary> Greetings </summary>

Hello!

This is my first PR for `osu!`, I hope you'll welcome me with as much
enthusiasm as I have opening this!

The following PR comes from some frustration on my end when using song
select. I have not found issues or opened/closed PRs that address this
topic, so I thought I'd shoot my shot!

To give some more context, I've been pretty inactive on `osu!` and
playing almost exclusively songs I imported from stable. From what I
understand, this makes all my beatmapsets have the same `DateAdded`.
This triggers the `ID` fallback comparison, which, as noted in the code
comments, is essentially random.

Writing this, I realize that I haven't checked if this addresses a
behavior that changes from stable to lazer, or something that was always
here!

</details>

This PR aims at making a small part of song-select sorting more
intuitive by using the song's title as a fallback sorting method.

My feeling is that `DateAdded` "looks more random" than the title,
especially in cases like mine where the `DateAdded` fallback-comparison
outputs `0`.

The implementation is the same as `Artist` with `Title` fallback.
 
<details>

<summary> Screenshots </summary>

On my installed osu version: sorting by `BPM` puts "Asymetry" in the
middle of two mapsets of "Snow halation"

<img width="1053" height="841" alt="image"
src="https://github.com/user-attachments/assets/00a89251-5695-43b5-a388-248404c24f02"
/>

After the patch, mapsets with the same BPM will also be sorted by song
title, for example at 173 BPM:

<img width="1033" height="1177" alt="image"
src="https://github.com/user-attachments/assets/de7de0b9-f2aa-4e81-80f8-459bfcf6dc95"
/>

And further below:

<img width="1050" height="929" alt="image"
src="https://github.com/user-attachments/assets/dfdee7ad-7b5b-45c7-abfe-22ce3c561dd1"
/>

</details>

---------

Co-authored-by: Kenny Lorin <kenny.lorin@enioka.com>
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
This commit is contained in:
Kenny Lorin
2026-06-11 13:21:42 +02:00
committed by GitHub
Unverified
parent 3b4dfd5903
commit e484cf4553
3 changed files with 46 additions and 6 deletions
@@ -90,6 +90,42 @@ namespace osu.Game.Tests.Visual.SongSelect
Assert.That(results.TakeWhile(b => b.BeatmapSet!.DateSubmitted != null).Count(), Is.EqualTo(30), () => "non-missing dates should be at the start");
}
[Test]
public async Task TestSortByBpmUsesTitleAsTiebreaker()
{
List<BeatmapSetInfo> beatmapSets = [];
// 2 sets with same BPM but different titles
const int diff_count = 1;
{
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
set.DateAdded = new DateTimeOffset(2025, 6, 11, 10, 0, 0, TimeSpan.Zero);
set.Beatmaps.ForEach(b =>
{
b.ID = Guid.Parse("00000000-0000-0000-0000-000000000000");
b.BPM = 175;
b.Metadata.Title = "ZZZ";
});
beatmapSets.Add(set);
}
{
var set = TestResources.CreateTestBeatmapSetInfo(diff_count);
set.DateAdded = new DateTimeOffset(2025, 6, 10, 10, 0, 0, TimeSpan.Zero);
set.Beatmaps.ForEach(b =>
{
b.ID = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff");
b.BPM = 175;
b.Metadata.Title = "AAA";
});
beatmapSets.Add(set);
}
var results = (await runSorting(SortMode.BPM, beatmapSets)).ToList();
Assert.That(results[0].Metadata.Title, Is.EqualTo("AAA"));
Assert.That(results[1].Metadata.Title, Is.EqualTo("ZZZ"));
}
[Test]
public async Task TestSortByArtistUsesTitleAsTiebreaker()
{
@@ -323,15 +323,16 @@ namespace osu.Game.Tests.Visual.SongSelect
SelectNextSet();
// both sets have a difficulty with 0.00* star rating.
// in the case of a tie when sorting, the first tie-breaker is `DateAdded` descending, which will pick the last set added (see `TestResources.CreateTestBeatmapSetInfo()`).
WaitForSetSelection(1, 0);
// in the case of a tie when sorting, the first tie-breaker is `Title` ascending, which will pick the first set added as the title contains the set ID
// (see `TestResources.CreateTestBeatmapSetInfo()`).
WaitForSetSelection(0, 0);
SelectNextSet();
WaitForSetSelection(0, 0);
WaitForSetSelection(1, 0);
SelectNextPanel();
Select();
WaitForSetSelection(1, 1);
WaitForSetSelection(0, 1);
}
[Test]
@@ -115,12 +115,15 @@ namespace osu.Game.Screens.Select
throw new ArgumentOutOfRangeException();
}
// If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion.
// If the initial sort could not differentiate, attempt to use Title then DateAdded to order sets in a stable fashion.
if (comparison == 0)
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(a.BeatmapSet!.Metadata.Title, b.BeatmapSet!.Metadata.Title);
// The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc).
if (comparison == 0)
comparison = b.BeatmapSet!.DateAdded.CompareTo(a.BeatmapSet!.DateAdded);
// If DateAdded fails to break the tie, fallback to our internal GUID for stability.
// If both failed to break the tie, fallback to our internal GUID for stability.
// This basically means it's a stable random sort.
if (comparison == 0)
comparison = b.BeatmapSet!.ID.CompareTo(a.BeatmapSet!.ID);