diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs index 33f4d577bd..664eca6e23 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs @@ -8,11 +8,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; @@ -90,6 +92,43 @@ public void TestNoUserBest() AddAssert("no best score displayed", () => scoresContainer.ChildrenOfType().Count() == 1); } + [Test] + public void TestHitResultsWithSameNameAreGrouped() + { + AddStep("Load scores without user best", () => + { + var allScores = createScores(); + allScores.UserScore = null; + scoresContainer.Scores = allScores; + }); + + AddUntilStep("wait for scores displayed", () => scoresContainer.ChildrenOfType().Any()); + AddAssert("only one column for slider end", () => scoresContainer.ScoreTable.Columns.Count(c => c.Header.Equals("slider end")) == 1); + + AddAssert("all rows show non-zero slider ends", () => + { + int sliderEndColumnIndex = Array.FindIndex(scoresContainer.ScoreTable.Columns, c => c != null && c.Header.Equals("slider end")); + bool sliderEndFilledInEachRow = true; + + for (int i = 0; i < scoresContainer.ScoreTable.Content?.GetLength(0); i++) + { + switch (scoresContainer.ScoreTable.Content[i, sliderEndColumnIndex]) + { + case OsuSpriteText text: + if (text.Text.Equals(0.0d.ToLocalisableString(@"N0"))) + sliderEndFilledInEachRow = false; + break; + + default: + sliderEndFilledInEachRow = false; + break; + } + } + + return sliderEndFilledInEachRow; + }); + } + [Test] public void TestUserBest() { @@ -287,13 +326,17 @@ private APIScoresCollection createScores() const int initial_great_count = 2000; const int initial_tick_count = 100; + const int initial_slider_end_count = 500; int greatCount = initial_great_count; int tickCount = initial_tick_count; + int sliderEndCount = initial_slider_end_count; - foreach (var s in scores.Scores) + foreach (var (score, index) in scores.Scores.Select((s, i) => (s, i))) { - s.Statistics = new Dictionary + HitResult sliderEndResult = index % 2 == 0 ? HitResult.SliderTailHit : HitResult.SmallTickHit; + + score.Statistics = new Dictionary { { HitResult.Great, greatCount }, { HitResult.LargeTickHit, tickCount }, @@ -301,10 +344,19 @@ private APIScoresCollection createScores() { HitResult.Meh, RNG.Next(100) }, { HitResult.Miss, initial_great_count - greatCount }, { HitResult.LargeTickMiss, initial_tick_count - tickCount }, + { sliderEndResult, sliderEndCount }, + }; + + // Some hit results, including SliderTailHit and SmallTickHit, are only displayed + // when the maximum number is known + score.MaximumStatistics = new Dictionary + { + { sliderEndResult, initial_slider_end_count }, }; greatCount -= 100; tickCount -= RNG.Next(1, 5); + sliderEndCount -= 20; } return scores; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index a6868efb5d..671b5df33b 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -9,7 +9,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Framework.Extensions.EnumExtensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -58,9 +57,11 @@ public ScoreTable() } /// - /// The statistics that appear in the table, in order of appearance. + /// The statistics that appear in the table, grouped by their display name. If multiple HitResults have the same + /// DisplayName (for example, "slider end" is the name for both HitResult.SliderTailTick and HitResult.SmallTickHit + /// in osu!std) the name will only be listed once. /// - private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>(); + private readonly List<(LocalisableString displayName, IEnumerable results)> statisticResults = new List<(LocalisableString displayName, IEnumerable results)>(); private bool showPerformancePoints; @@ -72,7 +73,7 @@ public void DisplayScores(IReadOnlyList scores, bool showPerformanceC return; showPerformancePoints = showPerformanceColumn; - statisticResultTypes.Clear(); + statisticResults.Clear(); for (int i = 0; i < scores.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); @@ -105,20 +106,18 @@ private TableColumn[] createHeaders(IReadOnlyList scores) var ruleset = scores.First().Ruleset.CreateInstance(); - foreach (var result in EnumExtensions.GetValuesInOrder()) + foreach (var resultGroup in ruleset.GetHitResults().GroupBy(r => r.displayName)) { - if (!allScoreStatistics.Contains(result)) + if (!resultGroup.Any(r => allScoreStatistics.Contains(r.result))) continue; // for the time being ignore bonus result types. // this is not being sent from the API and will be empty in all cases. - if (result.IsBonus()) + if (resultGroup.All(r => r.result.IsBonus())) continue; - var displayName = ruleset.GetDisplayNameForHitResult(result); - - columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60))); - statisticResultTypes.Add((result, displayName)); + columns.Add(new TableColumn(resultGroup.Key, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60))); + statisticResults.Add((resultGroup.Key, resultGroup.Select(r => r.result))); } if (showPerformancePoints) @@ -167,12 +166,17 @@ private Drawable[] createContent(int index, ScoreInfo score) #pragma warning restore 618 }; - var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result); + var availableStatistics = score.GetStatisticsForDisplay().ToLookup(touple => touple.DisplayName); - foreach (var result in statisticResultTypes) + foreach (var (columnName, resultTypes) in statisticResults) { - if (!availableStatistics.TryGetValue(result.result, out var stat)) - stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName); + HitResultDisplayStatistic stat = new HitResultDisplayStatistic(resultTypes.First(), 0, null, columnName); + + if (availableStatistics.Contains(columnName)) + { + foreach (var s in availableStatistics[columnName]) + stat = s; + } content.Add(new StatisticText(stat.Count, stat.MaxCount, @"N0") { Colour = stat.Count == 0 ? Color4.Gray : Color4.White }); } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index b53b7826f3..39491e338e 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -34,7 +34,7 @@ public partial class ScoresContainer : BeatmapSetLayoutSection private readonly IBindable user = new Bindable(); private readonly Box background; - private readonly ScoreTable scoreTable; + public readonly ScoreTable ScoreTable; private readonly FillFlowContainer topScoresContainer; private readonly LoadingLayer loading; private readonly LeaderboardModSelector modSelector; @@ -59,8 +59,8 @@ protected APIScoresCollection Scores loadCancellationSource = new CancellationTokenSource(); topScoresContainer.Clear(); - scoreTable.ClearScores(); - scoreTable.Hide(); + ScoreTable.ClearScores(); + ScoreTable.Hide(); loading.Hide(); loading.FinishTransforms(); @@ -85,8 +85,8 @@ protected APIScoresCollection Scores var scores = value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).OrderByTotalScore().ToArray(); var topScore = scores.First(); - scoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); - scoreTable.Show(); + ScoreTable.DisplayScores(scores, apiBeatmap.Status.GrantsPerformancePoints()); + ScoreTable.Show(); var userScore = value.UserScore; var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo); @@ -175,7 +175,7 @@ public ScoresContainer() Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), }, - scoreTable = new ScoreTable + ScoreTable = new ScoreTable { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre,