diff --git a/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs new file mode 100644 index 0000000000..ae999d08d5 --- /dev/null +++ b/osu.Game.Tests/NonVisual/RulesetInfoOrderingTest.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class RulesetInfoOrderingTest + { + [Test] + public void TestOrdering() + { + var rulesets = new[] + { + new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1), + new OsuRuleset().RulesetInfo, + new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1), + new RulesetInfo("custom2", "Custom Ruleset 2", string.Empty, -1), + new CatchRuleset().RulesetInfo, + new RulesetInfo("custom3", "Custom Ruleset 3", string.Empty, -1), + }; + + var orderedRulesets = rulesets.OrderBy(r => r); + + // Ensure all customs are after official. + Assert.That(orderedRulesets.Select(r => r.OnlineID), Is.EqualTo(new[] { 0, 2, -1, -1, -1, -1 })); + + // Ensure customs are grouped next to each other (ie. stably sorted). + Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom2").Skip(1).First().ShortName, Is.EqualTo("custom2")); + Assert.That(orderedRulesets.SkipWhile(r => r.ShortName != "custom3").Skip(1).First().ShortName, Is.EqualTo("custom3")); + } + } +} diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index 9559cfa00c..ba56adac49 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets public bool Equals(EFRulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + public int CompareTo(RulesetInfo other) => OnlineID.CompareTo(other.OnlineID); + public override bool Equals(object obj) => obj is EFRulesetInfo rulesetInfo && Equals(rulesetInfo); public bool Equals(IRulesetInfo other) => other is RulesetInfo b && Equals(b); diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index 6599e0d59d..44731a2495 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets /// /// A representation of a ruleset's metadata. /// - public interface IRulesetInfo : IHasOnlineID, IEquatable + public interface IRulesetInfo : IHasOnlineID, IEquatable, IComparable { /// /// The user-exposed name of this ruleset. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index f9fa8966a8..0a0941d1ff 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -49,6 +49,20 @@ namespace osu.Game.Rulesets public bool Equals(IRulesetInfo? other) => other is RulesetInfo b && Equals(b); + public int CompareTo(RulesetInfo other) + { + if (OnlineID >= 0 && other.OnlineID >= 0) + return OnlineID.CompareTo(other.OnlineID); + + // Official rulesets are always given precedence for the time being. + if (OnlineID >= 0) + return -1; + if (other.OnlineID >= 0) + return 1; + + return string.Compare(ShortName, other.ShortName, StringComparison.Ordinal); + } + public override int GetHashCode() { // Importantly, ignore the underlying realm hash code, as it will usually not match. diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 0f518be639..d017d54ed9 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -163,10 +163,7 @@ namespace osu.Game.Rulesets } } - // add known official rulesets first.. - availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID >= 0).OrderBy(r => r.OnlineID)); - // .. then add any customs - availableRulesets.AddRange(detachedRulesets.Where(r => r.OnlineID < 0).OrderBy(r => r.ShortName)); + availableRulesets.AddRange(detachedRulesets.OrderBy(r => r)); }); } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 1f346d19f0..fd6a869938 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -89,7 +89,8 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - int ruleset = BeatmapInfo.Ruleset.OnlineID.CompareTo(otherBeatmap.BeatmapInfo.Ruleset.OnlineID); + int ruleset = BeatmapInfo.Ruleset.CompareTo(otherBeatmap.BeatmapInfo.Ruleset); + if (ruleset != 0) return ruleset; return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 6156fea769..fc4b6c27f3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select.Carousel beatmapSet.Beatmaps .Where(b => !b.Hidden) - .OrderBy(b => b.Ruleset.OnlineID) + .OrderBy(b => b.Ruleset) .ThenBy(b => b.StarRating) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild);