From a7f8c5935dd611843ff92c9d4193a94281b16a98 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:36:56 +0900 Subject: [PATCH 001/100] Expose LowestSuccessfulHitResult() --- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 018b50bd3d..77acbd4137 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Scoring /// Retrieves the with the largest hit window that produces a successful hit. /// /// The lowest allowed successful . - protected HitResult LowestSuccessfulHitResult() + public HitResult LowestSuccessfulHitResult() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { From e98f51923a7c242cae1ec275726d2a18a82dd48b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:38:24 +0900 Subject: [PATCH 002/100] Add timing distribution to OsuScoreProcessor --- .../Scoring/OsuScoreProcessor.cs | 76 +++++++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 +++ 2 files changed, 88 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 79a6ea7e92..83339bd061 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,17 +1,93 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { + /// + /// The number of bins on each side of the timing distribution. + /// + private const int timing_distribution_bins = 25; + + /// + /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. + /// + private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; + + /// + /// The centre bin, with a timing distribution very close to/at 0. + /// + private const int timing_distribution_centre_bin_index = timing_distribution_bins; + + private TimingDistribution timingDistribution; + + public override void ApplyBeatmap(IBeatmap beatmap) + { + var hitWindows = CreateHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + timingDistribution = new TimingDistribution(total_timing_distribution_bins, hitWindows.WindowFor(hitWindows.LowestSuccessfulHitResult()) / timing_distribution_bins); + + base.ApplyBeatmap(beatmap); + } + + protected override void OnResultApplied(JudgementResult result) + { + base.OnResultApplied(result); + + if (result.IsHit) + { + int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); + timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; + } + } + + protected override void OnResultReverted(JudgementResult result) + { + base.OnResultReverted(result); + + if (result.IsHit) + { + int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); + timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; + } + } + + public override void PopulateScore(ScoreInfo score) + { + base.PopulateScore(score); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + timingDistribution.Bins.AsSpan().Clear(); + } + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); public override HitWindows CreateHitWindows() => new OsuHitWindows(); } + + public class TimingDistribution + { + public readonly int[] Bins; + public readonly double BinSize; + + public TimingDistribution(int binCount, double binSize) + { + Bins = new int[binCount]; + BinSize = binSize; + } + } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1f40f44dce..619547aef4 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Scoring } updateScore(); + + OnResultApplied(result); + } + + protected virtual void OnResultApplied(JudgementResult result) + { } protected sealed override void RevertResultInternal(JudgementResult result) @@ -154,6 +160,12 @@ namespace osu.Game.Rulesets.Scoring } updateScore(); + + OnResultReverted(result); + } + + protected virtual void OnResultReverted(JudgementResult result) + { } private void updateScore() From c7c94eb3fdd6c25363c9379bc9c881a407952171 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:38:50 +0900 Subject: [PATCH 003/100] Initial implementation of timing distribution graph --- .../TestSceneTimingDistributionGraph.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs new file mode 100644 index 0000000000..456ac19383 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -0,0 +1,103 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Scoring; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneTimingDistributionGraph : OsuTestScene + { + public TestSceneTimingDistributionGraph() + { + Add(new TimingDistributionGraph(createNormalDistribution()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 100) + }); + } + + private TimingDistribution createNormalDistribution() + { + var distribution = new TimingDistribution(51, 5); + + // We create an approximately-normal distribution of 51 elements by using the 13th binomial row (14 initial elements) and subdividing the inner values twice. + var row = new List { 1 }; + for (int i = 0; i < 13; i++) + row.Add(row[i] * (13 - i) / (i + 1)); + + // Each subdivision yields 2n-1 total elements, so first subdivision will contain 27 elements, and the second will contain 53 elements. + for (int div = 0; div < 2; div++) + { + var newRow = new List { 1 }; + + for (int i = 0; i < row.Count - 1; i++) + { + newRow.Add((row[i] + row[i + 1]) / 2); + newRow.Add(row[i + 1]); + } + + row = newRow; + } + + // After the subdivisions take place, we're left with 53 values which we use the inner 51 of. + for (int i = 1; i < row.Count - 1; i++) + distribution.Bins[i - 1] = row[i]; + + return distribution; + } + } + + public class TimingDistributionGraph : CompositeDrawable + { + private readonly TimingDistribution distribution; + + public TimingDistributionGraph(TimingDistribution distribution) + { + this.distribution = distribution; + } + + [BackgroundDependencyLoader] + private void load() + { + int maxCount = distribution.Bins.Max(); + + var bars = new Drawable[distribution.Bins.Length]; + for (int i = 0; i < bars.Length; i++) + bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + }; + } + + private class Bar : CompositeDrawable + { + public Bar() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + + Padding = new MarginPadding { Horizontal = 1 }; + + InternalChild = new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#66FFCC") + }; + } + } + } +} From 6217fb26daf0a88c9c8bd867f1efb533c0c5c1bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 18:50:25 +0900 Subject: [PATCH 004/100] Finish up design implementation of timing distribution graph --- .../TestSceneTimingDistributionGraph.cs | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 456ac19383..4129975166 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -8,6 +8,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Scoring; using osuTK; @@ -21,7 +23,7 @@ namespace osu.Game.Tests.Visual.Ranking { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(300, 100) + Size = new Vector2(400, 130) }); } @@ -58,6 +60,17 @@ namespace osu.Game.Tests.Visual.Ranking public class TimingDistributionGraph : CompositeDrawable { + /// + /// The number of data points shown on the axis below the graph. + /// + private const float axis_points = 5; + + /// + /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. + /// Without an inset, the final data point will be placed halfway outside the graph. + /// + private const float axis_value_inset = 0.2f; + private readonly TimingDistribution distribution; public TimingDistributionGraph(TimingDistribution distribution) @@ -74,11 +87,79 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < bars.Length; i++) bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + Container axisFlow; + InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, - Content = new[] { bars } + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + } + }, + new Drawable[] + { + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } }; + + // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. + // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + int sideBins = (distribution.Bins.Length - 1) / 2; + double maxValue = sideBins * distribution.BinSize; + double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + } } private class Bar : CompositeDrawable From c9469dc0ddaf98b6d119e01581e09b7c209720fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 22:48:43 +0900 Subject: [PATCH 005/100] Add background --- .../TestSceneTimingDistributionGraph.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 4129975166..73225ff599 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -19,12 +19,20 @@ namespace osu.Game.Tests.Visual.Ranking { public TestSceneTimingDistributionGraph() { - Add(new TimingDistributionGraph(createNormalDistribution()) + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(400, 130) - }); + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new TimingDistributionGraph(createNormalDistribution()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(400, 130) + } + }; } private TimingDistribution createNormalDistribution() From ce56c457218879b78b14bd1226c300e27731e194 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 22:48:52 +0900 Subject: [PATCH 006/100] Implement the accuracy heatmap --- .../Ranking/TestSceneAccuracyHeatmap.cs | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs new file mode 100644 index 0000000000..8386ee5992 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -0,0 +1,273 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene + { + private readonly Box background; + private readonly Drawable object1; + private readonly Drawable object2; + private readonly Heatmap heatmap; + + public TestSceneAccuracyHeatmap() + { + Children = new[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333"), + }, + object1 = new BorderCircle + { + Position = new Vector2(256, 192), + Colour = Color4.Yellow, + }, + object2 = new BorderCircle + { + Position = new Vector2(500, 300), + }, + heatmap = new Heatmap + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(() => + { + var randomPos = new Vector2( + RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), + RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); + + // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). + heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); + }, 1, true); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + heatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); + return true; + } + + private class Heatmap : CompositeDrawable + { + /// + /// Full size of the heatmap. + /// + private const float size = 100; + + /// + /// Size of the inner circle containing the "hit" points, relative to . + /// All other points outside of the inner circle are "miss" points. + /// + private const float inner_portion = 0.8f; + + private const float rotation = 45; + private const float point_size = 4; + + private Container allPoints; + + public Heatmap() + { + Size = new Vector2(size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } + } + }, + allPoints = new Container { RelativeSizeAxes = Axes.Both } + }; + + Vector2 centre = new Vector2(size / 2); + int rows = (int)Math.Ceiling(size / point_size); + int cols = (int)Math.Ceiling(size / point_size); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Vector2 pos = new Vector2(c * point_size, r * point_size); + HitType type = HitType.Hit; + + if (Vector2.Distance(pos, centre) > size * inner_portion / 2) + type = HitType.Miss; + + allPoints.Add(new HitPoint(pos, type) + { + Size = new Vector2(point_size), + Colour = type == HitType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + }); + } + } + } + + public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + { + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. + double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. + double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. + + float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + + // Find the most relevant hit point. + double minDist = double.PositiveInfinity; + HitPoint point = null; + + foreach (var p in allPoints) + { + Vector2 localCentre = new Vector2(size / 2); + float localRadius = localCentre.X * inner_portion * normalisedDistance; + double localAngle = finalAngle + 3 * Math.PI / 4; + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + + float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); + + if (dist < minDist) + { + minDist = dist; + point = p; + } + } + + Debug.Assert(point != null); + point.Increment(); + } + } + + private class HitPoint : Circle + { + private readonly HitType type; + + public HitPoint(Vector2 position, HitType type) + { + this.type = type; + + Position = position; + Alpha = 0; + } + + public void Increment() + { + if (Alpha < 1) + Alpha += 0.1f; + else if (type == HitType.Hit) + Colour = ((Color4)Colour).Lighten(0.1f); + } + } + + private enum HitType + { + Hit, + Miss + } + + private class BorderCircle : CircularContainer + { + public BorderCircle() + { + Origin = Anchor.Centre; + Size = new Vector2(100); + + Masking = true; + BorderThickness = 2; + BorderColour = Color4.White; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(4), + } + }; + } + } + } +} From 6000e0f86a48e89d6e64f14cb4f9b5046f04c486 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 23:01:22 +0900 Subject: [PATCH 007/100] Increase size to match timing distribution graph --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 8386ee5992..b605ddcc35 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Ranking /// /// Full size of the heatmap. /// - private const float size = 100; + private const float size = 130; /// /// Size of the inner circle containing the "hit" points, relative to . From b663c940aea1da0d66b8cea0c86637edaacf81d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 23:46:46 +0900 Subject: [PATCH 008/100] Rename enum --- .../Visual/Ranking/TestSceneAccuracyHeatmap.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index b605ddcc35..9f82287640 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -167,15 +167,15 @@ namespace osu.Game.Tests.Visual.Ranking for (int c = 0; c < cols; c++) { Vector2 pos = new Vector2(c * point_size, r * point_size); - HitType type = HitType.Hit; + HitPointType pointType = HitPointType.Hit; if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - type = HitType.Miss; + pointType = HitPointType.Miss; - allPoints.Add(new HitPoint(pos, type) + allPoints.Add(new HitPoint(pos, pointType) { Size = new Vector2(point_size), - Colour = type == HitType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) }); } } @@ -216,11 +216,11 @@ namespace osu.Game.Tests.Visual.Ranking private class HitPoint : Circle { - private readonly HitType type; + private readonly HitPointType pointType; - public HitPoint(Vector2 position, HitType type) + public HitPoint(Vector2 position, HitPointType pointType) { - this.type = type; + this.pointType = pointType; Position = position; Alpha = 0; @@ -230,12 +230,12 @@ namespace osu.Game.Tests.Visual.Ranking { if (Alpha < 1) Alpha += 0.1f; - else if (type == HitType.Hit) + else if (pointType == HitPointType.Hit) Colour = ((Color4)Colour).Lighten(0.1f); } } - private enum HitType + private enum HitPointType { Hit, Miss From f9db37a1de9c425661df474f2d56fbf2d9ddfa27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 21:48:59 +0900 Subject: [PATCH 009/100] Split out types --- .../Scoring/OsuScoreProcessor.cs | 18 -- .../Scoring/TimingDistribution.cs | 17 ++ osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 186 ++++++++++++++++++ .../Statistics/TimingDistributionGraph.cs | 139 +++++++++++++ .../Ranking/TestSceneAccuracyHeatmap.cs | 175 +--------------- .../TestSceneTimingDistributionGraph.cs | 130 +----------- 6 files changed, 344 insertions(+), 321 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs create mode 100644 osu.Game.Rulesets.Osu/Statistics/Heatmap.cs create mode 100644 osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 83339bd061..a9d48df52d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -62,11 +61,6 @@ namespace osu.Game.Rulesets.Osu.Scoring } } - public override void PopulateScore(ScoreInfo score) - { - base.PopulateScore(score); - } - protected override void Reset(bool storeResults) { base.Reset(storeResults); @@ -78,16 +72,4 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } - - public class TimingDistribution - { - public readonly int[] Bins; - public readonly double BinSize; - - public TimingDistribution(int binCount, double binSize) - { - Bins = new int[binCount]; - BinSize = binSize; - } - } } diff --git a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs new file mode 100644 index 0000000000..46f259f3d8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class TimingDistribution + { + public readonly int[] Bins; + public readonly double BinSize; + + public TimingDistribution(int binCount, double binSize) + { + Bins = new int[binCount]; + BinSize = binSize; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs new file mode 100644 index 0000000000..8105d12991 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -0,0 +1,186 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Statistics +{ + public class Heatmap : CompositeDrawable + { + /// + /// Full size of the heatmap. + /// + private const float size = 130; + + /// + /// Size of the inner circle containing the "hit" points, relative to . + /// All other points outside of the inner circle are "miss" points. + /// + private const float inner_portion = 0.8f; + + private const float rotation = 45; + private const float point_size = 4; + + private Container allPoints; + + public Heatmap() + { + Size = new Vector2(size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } + } + }, + allPoints = new Container { RelativeSizeAxes = Axes.Both } + }; + + Vector2 centre = new Vector2(size / 2); + int rows = (int)Math.Ceiling(size / point_size); + int cols = (int)Math.Ceiling(size / point_size); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Vector2 pos = new Vector2(c * point_size, r * point_size); + HitPointType pointType = HitPointType.Hit; + + if (Vector2.Distance(pos, centre) > size * inner_portion / 2) + pointType = HitPointType.Miss; + + allPoints.Add(new HitPoint(pos, pointType) + { + Size = new Vector2(point_size), + Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + }); + } + } + } + + public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + { + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. + double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. + double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. + + float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + + // Find the most relevant hit point. + double minDist = double.PositiveInfinity; + HitPoint point = null; + + foreach (var p in allPoints) + { + Vector2 localCentre = new Vector2(size / 2); + float localRadius = localCentre.X * inner_portion * normalisedDistance; + double localAngle = finalAngle + 3 * Math.PI / 4; + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + + float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); + + if (dist < minDist) + { + minDist = dist; + point = p; + } + } + + Debug.Assert(point != null); + point.Increment(); + } + + private class HitPoint : Circle + { + private readonly HitPointType pointType; + + public HitPoint(Vector2 position, HitPointType pointType) + { + this.pointType = pointType; + + Position = position; + Alpha = 0; + } + + public void Increment() + { + if (Alpha < 1) + Alpha += 0.1f; + else if (pointType == HitPointType.Hit) + Colour = ((Color4)Colour).Lighten(0.1f); + } + } + + private enum HitPointType + { + Hit, + Miss + } + } +} diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs new file mode 100644 index 0000000000..a47d726988 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -0,0 +1,139 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Statistics +{ + public class TimingDistributionGraph : CompositeDrawable + { + /// + /// The number of data points shown on the axis below the graph. + /// + private const float axis_points = 5; + + /// + /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. + /// Without an inset, the final data point will be placed halfway outside the graph. + /// + private const float axis_value_inset = 0.2f; + + private readonly TimingDistribution distribution; + + public TimingDistributionGraph(TimingDistribution distribution) + { + this.distribution = distribution; + } + + [BackgroundDependencyLoader] + private void load() + { + int maxCount = distribution.Bins.Max(); + + var bars = new Drawable[distribution.Bins.Length]; + for (int i = 0; i < bars.Length; i++) + bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + + Container axisFlow; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + } + }, + new Drawable[] + { + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; + + // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. + // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + int sideBins = (distribution.Bins.Length - 1) / 2; + double maxValue = sideBins * distribution.BinSize; + double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + } + } + + private class Bar : CompositeDrawable + { + public Bar() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + + Padding = new MarginPadding { Horizontal = 1 }; + + InternalChild = new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#66FFCC") + }; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 9f82287640..9e5fda0ae6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,15 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Diagnostics; -using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Osu.Statistics; using osuTK; using osuTK.Graphics; @@ -70,177 +68,6 @@ namespace osu.Game.Tests.Visual.Ranking return true; } - private class Heatmap : CompositeDrawable - { - /// - /// Full size of the heatmap. - /// - private const float size = 130; - - /// - /// Size of the inner circle containing the "hit" points, relative to . - /// All other points outside of the inner circle are "miss" points. - /// - private const float inner_portion = 0.8f; - - private const float rotation = 45; - private const float point_size = 4; - - private Container allPoints; - - public Heatmap() - { - Size = new Vector2(size); - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(inner_portion), - Masking = true, - BorderThickness = 2f, - BorderColour = Color4.White, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#202624") - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 10, - Height = 2f, - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Y = -1, - Width = 2f, - Height = 10, - } - } - }, - allPoints = new Container { RelativeSizeAxes = Axes.Both } - }; - - Vector2 centre = new Vector2(size / 2); - int rows = (int)Math.Ceiling(size / point_size); - int cols = (int)Math.Ceiling(size / point_size); - - for (int r = 0; r < rows; r++) - { - for (int c = 0; c < cols; c++) - { - Vector2 pos = new Vector2(c * point_size, r * point_size); - HitPointType pointType = HitPointType.Hit; - - if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - pointType = HitPointType.Miss; - - allPoints.Add(new HitPoint(pos, pointType) - { - Size = new Vector2(point_size), - Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) - }); - } - } - } - - public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) - { - double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. - double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. - double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. - - float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - - // Find the most relevant hit point. - double minDist = double.PositiveInfinity; - HitPoint point = null; - - foreach (var p in allPoints) - { - Vector2 localCentre = new Vector2(size / 2); - float localRadius = localCentre.X * inner_portion * normalisedDistance; - double localAngle = finalAngle + 3 * Math.PI / 4; - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); - - float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); - - if (dist < minDist) - { - minDist = dist; - point = p; - } - } - - Debug.Assert(point != null); - point.Increment(); - } - } - - private class HitPoint : Circle - { - private readonly HitPointType pointType; - - public HitPoint(Vector2 position, HitPointType pointType) - { - this.pointType = pointType; - - Position = position; - Alpha = 0; - } - - public void Increment() - { - if (Alpha < 1) - Alpha += 0.1f; - else if (pointType == HitPointType.Hit) - Colour = ((Color4)Colour).Lighten(0.1f); - } - } - - private enum HitPointType - { - Hit, - Miss - } - private class BorderCircle : CircularContainer { public BorderCircle() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 73225ff599..7530fc42b8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -2,15 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.Statistics; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -65,128 +61,4 @@ namespace osu.Game.Tests.Visual.Ranking return distribution; } } - - public class TimingDistributionGraph : CompositeDrawable - { - /// - /// The number of data points shown on the axis below the graph. - /// - private const float axis_points = 5; - - /// - /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. - /// Without an inset, the final data point will be placed halfway outside the graph. - /// - private const float axis_value_inset = 0.2f; - - private readonly TimingDistribution distribution; - - public TimingDistributionGraph(TimingDistribution distribution) - { - this.distribution = distribution; - } - - [BackgroundDependencyLoader] - private void load() - { - int maxCount = distribution.Bins.Max(); - - var bars = new Drawable[distribution.Bins.Length]; - for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; - - Container axisFlow; - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { bars } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - }, - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }; - - // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. - // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - int sideBins = (distribution.Bins.Length - 1) / 2; - double maxValue = sideBins * distribution.BinSize; - double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "0", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - - for (int i = 1; i <= axis_points; i++) - { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - } - } - - private class Bar : CompositeDrawable - { - public Bar() - { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - RelativeSizeAxes = Axes.Both; - - Padding = new MarginPadding { Horizontal = 1 }; - - InternalChild = new Circle - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#66FFCC") - }; - } - } - } } From 900da8849866bc13ddc2e71c4065d24c4ed4a74c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 22:44:55 +0900 Subject: [PATCH 010/100] Populate hit offsets from score processor --- .../Judgements/OsuHitCircleJudgementResult.cs | 23 +++++++ .../Objects/Drawables/DrawableHitCircle.cs | 28 ++++++++- osu.Game.Rulesets.Osu/Scoring/HitOffset.cs | 23 +++++++ .../Scoring/OsuScoreProcessor.cs | 61 ++++++++++++++++++- osu.Game/Scoring/ScoreInfo.cs | 2 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs create mode 100644 osu.Game.Rulesets.Osu/Scoring/HitOffset.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs new file mode 100644 index 0000000000..103d02958d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuHitCircleJudgementResult : OsuJudgementResult + { + public HitCircle HitCircle => (HitCircle)HitObject; + + public Vector2? HitPosition; + public float? Radius; + + public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement) + : base(hitObject, judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index d73ad888f4..2f86400b25 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -7,8 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osuTK; @@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; + private InputManager inputManager; + public DrawableHitCircle(HitCircle h) : base(h) { @@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + public override double LifetimeStart { get => base.LifetimeStart; @@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - ApplyResult(r => r.Type = result); + ApplyResult(r => + { + var circleResult = (OsuHitCircleJudgementResult)r; + + if (result != HitResult.Miss) + { + var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); + circleResult.HitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); + circleResult.Radius = (float)HitObject.Radius; + } + + circleResult.Type = result; + }); } protected override void UpdateInitialTransforms() @@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => ApproachCircle; + protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement); + public class HitReceptor : CompositeDrawable, IKeyBindingHandler { // IsHovered is used diff --git a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs new file mode 100644 index 0000000000..e6a5a01b48 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class HitOffset + { + public readonly Vector2 Position1; + public readonly Vector2 Position2; + public readonly Vector2 HitPosition; + public readonly float Radius; + + public HitOffset(Vector2 position1, Vector2 position2, Vector2 hitPosition, float radius) + { + Position1 = position1; + Position2 = position2; + HitPosition = hitPosition; + Radius = radius; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index a9d48df52d..97be372e37 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Scoring private const int timing_distribution_centre_bin_index = timing_distribution_bins; private TimingDistribution timingDistribution; + private readonly List hitOffsets = new List(); public override void ApplyBeatmap(IBeatmap beatmap) { @@ -39,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Scoring base.ApplyBeatmap(beatmap); } + private OsuHitCircleJudgementResult lastCircleResult; + protected override void OnResultApplied(JudgementResult result) { base.OnResultApplied(result); @@ -47,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; + + addHitOffset(result); } } @@ -58,17 +67,67 @@ namespace osu.Game.Rulesets.Osu.Scoring { int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; + + removeHitOffset(result); } } + private void addHitOffset(JudgementResult result) + { + if (!(result is OsuHitCircleJudgementResult circleResult)) + return; + + if (lastCircleResult == null) + { + lastCircleResult = circleResult; + return; + } + + if (circleResult.HitPosition != null) + { + Debug.Assert(circleResult.Radius != null); + hitOffsets.Add(new HitOffset(lastCircleResult.HitCircle.StackedEndPosition, circleResult.HitCircle.StackedEndPosition, circleResult.HitPosition.Value, circleResult.Radius.Value)); + } + + lastCircleResult = circleResult; + } + + private void removeHitOffset(JudgementResult result) + { + if (!(result is OsuHitCircleJudgementResult circleResult)) + return; + + if (hitOffsets.Count > 0 && circleResult.HitPosition != null) + hitOffsets.RemoveAt(hitOffsets.Count - 1); + } + protected override void Reset(bool storeResults) { base.Reset(storeResults); timingDistribution.Bins.AsSpan().Clear(); + hitOffsets.Clear(); } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); + public override void PopulateScore(ScoreInfo score) + { + base.PopulateScore(score); + + score.ExtraStatistics["timing_distribution"] = timingDistribution; + score.ExtraStatistics["hit_offsets"] = hitOffsets; + } + + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) + { + switch (hitObject) + { + case HitCircle _: + return new OsuHitCircleJudgementResult(hitObject, judgement); + + default: + return new OsuJudgementResult(hitObject, judgement); + } + } public override HitWindows CreateHitWindows() => new OsuHitWindows(); } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7b37c267bc..38b37afc55 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -166,6 +166,8 @@ namespace osu.Game.Scoring } } + public Dictionary ExtraStatistics = new Dictionary(); + [JsonIgnore] public List Files { get; set; } From 89b54be67395cfdaa6ef6b37862e899545e54c72 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 22:45:18 +0900 Subject: [PATCH 011/100] Add initial implementation of the statistics panel --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 14 ++++ osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 10 ++- .../Ranking/TestSceneAccuracyHeatmap.cs | 4 +- .../Ranking/TestScreenStatisticsPanel.cs | 24 +++++++ osu.Game/Rulesets/Ruleset.cs | 3 + .../Ranking/Statistics/StatisticContainer.cs | 70 +++++++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 56 +++++++++++++++ 7 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 689a7b35ea..a76413480d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,8 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Osu { @@ -186,5 +188,17 @@ namespace osu.Game.Rulesets.Osu public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); + + public override IEnumerable CreateStatistics(ScoreInfo score) => new[] + { + new StatisticContainer("Timing Distribution") + { + Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + }, + new StatisticContainer("Accuracy Heatmap") + { + Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + }, + }; } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 8105d12991..89d861a6d1 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -29,10 +31,13 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; private const float point_size = 4; + private readonly IReadOnlyList offsets; private Container allPoints; - public Heatmap() + public Heatmap(IReadOnlyList offsets) { + this.offsets = offsets; + Size = new Vector2(size); } @@ -122,6 +127,9 @@ namespace osu.Game.Rulesets.Osu.Statistics }); } } + + foreach (var o in offsets) + AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); } public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 9e5fda0ae6..a1b2dccea3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; using osuTK; using osuTK.Graphics; @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new Heatmap + heatmap = new Heatmap(new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs new file mode 100644 index 0000000000..e61cf9568d --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestScreenStatisticsPanel : OsuTestScene + { + [Test] + public void TestScore() + { + loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + } + + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => + { + Child = new StatisticsPanel(score); + }); + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4f28607733..5c349ca557 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -23,6 +23,7 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets { @@ -208,5 +209,7 @@ namespace osu.Game.Rulesets /// /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + + public virtual IEnumerable CreateStatistics(ScoreInfo score) => Enumerable.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs new file mode 100644 index 0000000000..8d10529496 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticContainer : Container + { + protected override Container Content => content; + + private readonly Container content; + + public StatisticContainer(string name) + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + } + }, + new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs new file mode 100644 index 0000000000..8ab85527db --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticsPanel : CompositeDrawable + { + public StatisticsPanel(ScoreInfo score) + { + // Todo: Not correct. + RelativeSizeAxes = Axes.Both; + + Container statistics; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new ScorePanel(score) // Todo: Temporary + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + State = PanelState.Expanded, + X = 30 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, + Right = 50 + }, + Child = statistics = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(30, 15), + } + } + }; + + foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) + statistics.Add(s); + } + } +} From a65c1a9abdb358f69065e72b1ebe00ab2e1ee95a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 16:08:41 +0900 Subject: [PATCH 012/100] Fix test name --- ...TestScreenStatisticsPanel.cs => TestSceneStatisticsPanel.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Ranking/{TestScreenStatisticsPanel.cs => TestSceneStatisticsPanel.cs} (91%) diff --git a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs similarity index 91% rename from osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e61cf9568d..22ee6077cb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -8,7 +8,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestScreenStatisticsPanel : OsuTestScene + public class TestSceneStatisticsPanel : OsuTestScene { [Test] public void TestScore() From 9ea7c3dc90474e56bdf07c30988f1cd5007ef919 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 16:31:02 +0900 Subject: [PATCH 013/100] Make heatmap support dynamic sizing --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 155 +++++++++++------- .../Ranking/TestSceneAccuracyHeatmap.cs | 16 +- 2 files changed, 108 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 89d861a6d1..7e140e6fd2 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -18,12 +19,7 @@ namespace osu.Game.Rulesets.Osu.Statistics public class Heatmap : CompositeDrawable { /// - /// Full size of the heatmap. - /// - private const float size = 130; - - /// - /// Size of the inner circle containing the "hit" points, relative to . + /// Size of the inner circle containing the "hit" points, relative to the size of this . /// All other points outside of the inner circle are "miss" points. /// private const float inner_portion = 0.8f; @@ -34,77 +30,106 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly IReadOnlyList offsets; private Container allPoints; + private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); + public Heatmap(IReadOnlyList offsets) { this.offsets = offsets; - Size = new Vector2(size); + AddLayout(sizeLayout); } [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = new Container { - new CircularContainer + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(inner_portion), - Masking = true, - BorderThickness = 2f, - BorderColour = Color4.White, - Child = new Box + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#202624") - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box + Masking = true, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 10, - Height = 2f, - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Y = -1, - Width = 2f, - Height = 10, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } } + }, + allPoints = new Container + { + RelativeSizeAxes = Axes.Both } - }, - allPoints = new Container { RelativeSizeAxes = Axes.Both } + } }; + } + + protected override void Update() + { + base.Update(); + validateHitPoints(); + } + + private void validateHitPoints() + { + if (sizeLayout.IsValid) + return; + + allPoints.Clear(); + + // Since the content is fit, both dimensions should have the same size. + float size = allPoints.DrawSize.X; Vector2 centre = new Vector2(size / 2); int rows = (int)Math.Ceiling(size / point_size); @@ -130,16 +155,24 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var o in offsets) AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + + sizeLayout.Validate(); } - public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { + if (allPoints.Count == 0) + return; + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + // Since the content is fit, both dimensions should have the same size. + float size = allPoints.DrawSize.X; + // Find the most relevant hit point. double minDist = double.PositiveInfinity; HitPoint point = null; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index a1b2dccea3..53c8e56f53 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Ranking private readonly Box background; private readonly Drawable object1; private readonly Drawable object2; - private readonly Heatmap heatmap; + private readonly TestHeatmap heatmap; public TestSceneAccuracyHeatmap() { @@ -40,10 +40,11 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new Heatmap(new List()) + heatmap = new TestHeatmap(new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Size = new Vector2(130) } }; } @@ -70,6 +71,17 @@ namespace osu.Game.Tests.Visual.Ranking return true; } + private class TestHeatmap : Heatmap + { + public TestHeatmap(IReadOnlyList offsets) + : base(offsets) + { + } + + public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + => base.AddPoint(start, end, hitPoint, radius); + } + private class BorderCircle : CircularContainer { public BorderCircle() From 3dbe164b2c4da72b25c26d25156bc5a8de3ce28b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:20:38 +0900 Subject: [PATCH 014/100] Add some very basic safety checks around non-existent data --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 7 +++++-- .../Statistics/TimingDistributionGraph.cs | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 7e140e6fd2..51508a5e8b 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -153,8 +153,11 @@ namespace osu.Game.Rulesets.Osu.Statistics } } - foreach (var o in offsets) - AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + if (offsets?.Count > 0) + { + foreach (var o in offsets) + AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + } sizeLayout.Validate(); } diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index a47d726988..0ba94b7101 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Statistics [BackgroundDependencyLoader] private void load() { + if (distribution?.Bins == null || distribution.Bins.Length == 0) + return; + int maxCount = distribution.Bins.Max(); var bars = new Drawable[distribution.Bins.Length]; From 076eac2362480a80749a0b42d1caaf7295f25ae3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:46:34 +0900 Subject: [PATCH 015/100] Inset entire graph rather than just the axis --- .../Statistics/TimingDistributionGraph.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 0ba94b7101..1f9f38bf3b 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Osu.Statistics /// private const float axis_points = 5; - /// - /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. - /// Without an inset, the final data point will be placed halfway outside the graph. - /// - private const float axis_value_inset = 0.2f; - private readonly TimingDistribution distribution; public TimingDistributionGraph(TimingDistribution distribution) @@ -50,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Statistics InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, + Width = 0.8f, Content = new[] { new Drawable[] @@ -80,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. int sideBins = (distribution.Bins.Length - 1) / 2; double maxValue = sideBins * distribution.BinSize; - double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + double axisValueStep = maxValue / axis_points; axisFlow.Add(new OsuSpriteText { From 9442fc00ac408c314ca078bbfa5d3c426adf6aa2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:48:59 +0900 Subject: [PATCH 016/100] Temporary hack to make replay player populate scores --- osu.Game/Screens/Play/ReplayPlayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index b443603128..d7580ea271 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -27,11 +27,14 @@ namespace osu.Game.Screens.Play protected override void GotoRanking() { - this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); + this.Push(CreateResults(CreateScore())); } protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - protected override ScoreInfo CreateScore() => score.ScoreInfo; + // protected override ScoreInfo CreateScore() + // { + // return score.ScoreInfo; + // } } } From a2ddb4edb456ed7b0c78ff4af1f67e4e3a312289 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:28 +0900 Subject: [PATCH 017/100] Change interface for creating statistic rows --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 37 +++++++++++++++---- osu.Game/Rulesets/Ruleset.cs | 2 +- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticRow.cs | 15 ++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 16 ++++++-- 5 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticRow.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a76413480d..67a9bda1a9 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -189,16 +190,36 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override IEnumerable CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] { - new StatisticContainer("Timing Distribution") + new StatisticRow { - Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) - }, - new StatisticContainer("Accuracy Heatmap") - { - Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) - }, + Content = new Drawable[] + { + new StatisticContainer("Timing Distribution") + { + RelativeSizeAxes = Axes.X, + Height = 130, + Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + { + RelativeSizeAxes = Axes.Both + } + }, + new StatisticContainer("Accuracy Heatmap") + { + RelativeSizeAxes = Axes.Both, + Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + { + RelativeSizeAxes = Axes.Both + } + }, + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 130), + } + } }; } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5c349ca557..f05685b6e9 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,6 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; - public virtual IEnumerable CreateStatistics(ScoreInfo score) => Enumerable.Empty(); + public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 8d10529496..d7b42c1c2f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Ranking.Statistics { InternalChild = new GridContainer { - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Content = new[] { new Drawable[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs new file mode 100644 index 0000000000..5d39ef57b2 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticRow + { + public Drawable[] Content = Array.Empty(); + public Dimension[] ColumnDimensions = Array.Empty(); + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8ab85527db..6c5fa1837a 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: Not correct. RelativeSizeAxes = Axes.Both; - Container statistics; + FillFlowContainer statisticRows; InternalChildren = new Drawable[] { @@ -41,16 +41,26 @@ namespace osu.Game.Screens.Ranking.Statistics Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, Right = 50 }, - Child = statistics = new FillFlowContainer + Child = statisticRows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), } } }; foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) - statistics.Add(s); + { + statisticRows.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { s.Content }, + ColumnDimensions = s.ColumnDimensions, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } } } } From 808e216059e00b52cf7c02a62ea1ca94bd5aaccd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:37 +0900 Subject: [PATCH 018/100] Improve test scene --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 13 ++++++++++++- .../Ranking/TestSceneTimingDistributionGraph.cs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 22ee6077cb..c02be9ab5d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -13,7 +15,16 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScore() { - loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) + { + ExtraStatistics = + { + ["timing_distribution"] = TestSceneTimingDistributionGraph.CreateNormalDistribution(), + ["hit_offsets"] = new List() + } + }; + + loadPanel(score); } private void loadPanel(ScoreInfo score) => AddStep("load panel", () => diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 7530fc42b8..2249655093 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(createNormalDistribution()) + new TimingDistributionGraph(CreateNormalDistribution()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Ranking }; } - private TimingDistribution createNormalDistribution() + public static TimingDistribution CreateNormalDistribution() { var distribution = new TimingDistribution(51, 5); From e7687a09271d12f5cadf93b00b7ff798733d9340 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:43 +0900 Subject: [PATCH 019/100] Temporary placement inside results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fbb9b95478..4d589b4527 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking.Statistics; using osuTK; namespace osu.Game.Screens.Ranking @@ -134,6 +135,11 @@ namespace osu.Game.Screens.Ranking }, }); } + + AddInternal(new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.Both + }); } protected override void LoadComplete() From 5e74985edafa034306c4559d32e99aa495697d80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Jun 2020 19:28:40 +0900 Subject: [PATCH 020/100] Add scrolling capability to results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 61 +++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4d589b4527..11ed9fb5b7 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Ranking public abstract class ResultsScreen : OsuScreen { protected const float BACKGROUND_BLUR = 20; + private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -68,10 +69,24 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = panels = new ScorePanelList + Child = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + panels = new ScorePanelList + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + SelectedScore = { BindTarget = SelectedScore } + }, + new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + } + } } } }, @@ -135,11 +150,6 @@ namespace osu.Game.Screens.Ranking }, }); } - - AddInternal(new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.Both - }); } protected override void LoadComplete() @@ -180,27 +190,38 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + [Cached] private class ResultsScrollContainer : OsuScrollContainer { - private readonly Container content; - - protected override Container Content => content; - public ResultsScrollContainer() { - base.Content.Add(content = new Container - { - RelativeSizeAxes = Axes.X - }); - RelativeSizeAxes = Axes.Both; ScrollbarVisible = false; } - protected override void Update() + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { - base.Update(); - content.Height = Math.Max(768 - TwoLayerButton.SIZE_EXTENDED.Y, DrawHeight); + if (!animated) + { + // If the user is scrolling via mouse drag, follow the mouse 1:1. + base.OnUserScroll(value, false, distanceDecay); + } + else + { + float direction = Math.Sign(value - Target); + float target = Target + direction * screen_height; + + if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) + { + // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. + base.OnUserScroll(value, true, distanceDecay); + } + else + { + // Otherwise, scroll one screen in the target direction. + base.OnUserScroll(target, true, distanceDecay); + } + } } } } From 725b2e540bdfd86d836c6345f6e6ea1daead0865 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Jun 2020 22:29:00 +0900 Subject: [PATCH 021/100] wip --- .../Visual/Ranking/TestSceneScorePanel.cs | 38 ++++++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 89 +++++++++++++------ osu.Game/Screens/Ranking/ScorePanel.cs | 60 +++++++++++++ osu.Game/Screens/Ranking/ScorePanelList.cs | 59 +++++++++--- .../Ranking/Statistics/StatisticsPanel.cs | 7 -- 5 files changed, 205 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 250fdc5ebd..1c5087ee94 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -101,6 +102,39 @@ namespace osu.Game.Tests.Visual.Ranking AddWaitStep("wait for transition", 10); } + [Test] + public void TestSceneTrackingScorePanel() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + + addPanelStep(score, PanelState.Contracted); + + AddStep("enable tracking", () => + { + panel.Anchor = Anchor.CentreLeft; + panel.Origin = Anchor.CentreLeft; + panel.Tracking = true; + + Add(panel.CreateTrackingComponent().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + })); + }); + + assertTracking(true); + + AddStep("expand panel", () => panel.State = PanelState.Expanded); + AddWaitStep("wait for transition", 2); + assertTracking(true); + + AddStep("stop tracking", () => panel.Tracking = false); + assertTracking(false); + + AddStep("start tracking", () => panel.Tracking = true); + assertTracking(true); + } + private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { Child = panel = new ScorePanel(score) @@ -110,5 +144,9 @@ namespace osu.Game.Tests.Visual.Ranking State = state }; }); + + private void assertTracking(bool tracking) => AddAssert($"{(tracking ? "is" : "is not")} tracking", () => + Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.TopLeft, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.TopLeft) == tracking + && Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.BottomRight, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.BottomRight) == tracking); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 11ed9fb5b7..4ef012f6f2 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -10,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -44,6 +46,9 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } + private Container scorePanelContainer; + private ResultsScrollContainer scrollContainer; + private Container expandedPanelProxyContainer; private Drawable bottomPanel; private ScorePanelList panels; @@ -58,6 +63,13 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { + scorePanelContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + FillFlowContainer buttons; InternalChild = new GridContainer @@ -67,26 +79,35 @@ namespace osu.Game.Screens.Ranking { new Drawable[] { - new ResultsScrollContainer + new Container { - Child = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + scorePanelContainer, + scrollContainer = new ResultsScrollContainer { - panels = new ScorePanelList + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, - Height = screen_height, - SelectedScore = { BindTarget = SelectedScore } - }, - new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.X, - Height = screen_height, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + panels = new ScorePanelList(scorePanelContainer) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + SelectedScore = { BindTarget = SelectedScore } + }, + new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + } + } } - } + }, + expandedPanelProxyContainer = new Container { RelativeSizeAxes = Axes.Both } } } }, @@ -173,6 +194,21 @@ namespace osu.Game.Screens.Ranking /// An responsible for the fetch operation. This will be queued and performed automatically. protected virtual APIRequest FetchScores(Action> scoresCallback) => null; + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + ScorePanel expandedPanel = scorePanelContainer.Single(p => p.State == PanelState.Expanded); + expandedPanel.Tracking = false; + expandedPanel.Anchor = Anchor.Centre; + expandedPanel.Origin = Anchor.Centre; + + scorePanelContainer.X = (float)Interpolation.Lerp(0, -DrawWidth / 2 + ScorePanel.EXPANDED_WIDTH / 2f, Math.Clamp(scrollContainer.Current / (screen_height * 0.8f), 0, 1)); + + if (expandedPanelProxyContainer.Count == 0) + expandedPanelProxyContainer.Add(expandedPanel.CreateProxy()); + } + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -205,22 +241,21 @@ namespace osu.Game.Screens.Ranking { // If the user is scrolling via mouse drag, follow the mouse 1:1. base.OnUserScroll(value, false, distanceDecay); + return; + } + + float direction = Math.Sign(value - Target); + float target = Target + direction * screen_height; + + if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) + { + // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. + base.OnUserScroll(value, true, distanceDecay); } else { - float direction = Math.Sign(value - Target); - float target = Target + direction * screen_height; - - if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) - { - // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. - base.OnUserScroll(value, true, distanceDecay); - } - else - { - // Otherwise, scroll one screen in the target direction. - base.OnUserScroll(target, true, distanceDecay); - } + // Otherwise, scroll one screen in the target direction. + base.OnUserScroll(target, true, distanceDecay); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 65fb901c89..7ca96a9a58 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -182,6 +182,40 @@ namespace osu.Game.Screens.Ranking } } + private bool tracking; + private Vector2 lastNonTrackingPosition; + + /// + /// Whether this should track the position of the tracking component created via . + /// + public bool Tracking + { + get => tracking; + set + { + if (tracking == value) + return; + + tracking = value; + + if (tracking) + lastNonTrackingPosition = Position; + else + Position = lastNonTrackingPosition; + } + } + + protected override void Update() + { + base.Update(); + + if (Tracking && trackingComponent != null) + { + Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); + Position = topLeftPos - AnchorPosition + OriginPosition; + } + } + private void updateState() { topLayerContent?.FadeOut(content_fade_duration).Expire(); @@ -248,5 +282,31 @@ namespace osu.Game.Screens.Ranking => base.ReceivePositionalInputAt(screenSpacePos) || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); + + private TrackingComponent trackingComponent; + + public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this); + + public class TrackingComponent : Drawable + { + public readonly ScorePanel Panel; + + public TrackingComponent(ScorePanel panel) + { + Panel = panel; + } + + protected override void Update() + { + base.Update(); + Size = Panel.DrawSize; + } + + // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. + // So by forwarding input events, we remove the need to consider the order in which input is handled. + protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); + } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 1142297274..d49085bc96 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,11 +28,20 @@ namespace osu.Game.Screens.Ranking public readonly Bindable SelectedScore = new Bindable(); + private readonly Container panels; private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; - public ScorePanelList() + /// + /// Creates a new . + /// + /// The target container in which s should reside. + /// s are set to track by default, but this allows + /// This should be placed _before_ the in the hierarchy. + /// + /// + public ScorePanelList(Container panelTarget = null) { RelativeSizeAxes = Axes.Both; @@ -47,6 +57,18 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Both, } }; + + if (panelTarget == null) + { + // To prevent 1-frame sizing issues, the panel container is added _before_ the scroll + flow containers + AddInternal(panels = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1 + }); + } + else + panels = panelTarget; } protected override void LoadComplete() @@ -62,10 +84,9 @@ namespace osu.Game.Screens.Ranking /// The to add. public void AddScore(ScoreInfo score) { - flow.Add(new ScorePanel(score) + var panel = new ScorePanel(score) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Tracking = true }.With(p => { p.StateChanged += s => @@ -73,6 +94,13 @@ namespace osu.Game.Screens.Ranking if (s == PanelState.Expanded) SelectedScore.Value = p.Score; }; + }); + + panels.Add(panel); + flow.Add(panel.CreateTrackingComponent().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; })); if (SelectedScore.Value == score) @@ -99,14 +127,15 @@ namespace osu.Game.Screens.Ranking private void selectedScoreChanged(ValueChangedEvent score) { // Contract the old panel. - foreach (var p in flow.Where(p => p.Score == score.OldValue)) + foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) { - p.State = PanelState.Contracted; - p.Margin = new MarginPadding(); + t.Panel.State = PanelState.Contracted; + t.Margin = new MarginPadding(); } // Find the panel corresponding to the new score. - expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue); + var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue); + expandedPanel = expandedTrackingComponent?.Panel; // handle horizontal scroll only when not hovering the expanded panel. scroll.HandleScroll = () => expandedPanel?.IsHovered != true; @@ -114,9 +143,11 @@ namespace osu.Game.Screens.Ranking if (expandedPanel == null) return; + Debug.Assert(expandedTrackingComponent != null); + // Expand the new panel. + expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; expandedPanel.State = PanelState.Expanded; - expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; // Scroll to the new panel. This is done manually since we need: // 1) To scroll after the scroll container's visible range is updated. @@ -145,15 +176,15 @@ namespace osu.Game.Screens.Ranking flow.Padding = new MarginPadding { Horizontal = offset }; } - private class Flow : FillFlowContainer + private class Flow : FillFlowContainer { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); - public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Score != score).Count(); + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); - private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() - .OrderByDescending(s => s.Score.TotalScore) - .ThenBy(s => s.Score.OnlineScoreID); + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Panel.Score.TotalScore) + .ThenBy(s => s.Panel.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 6c5fa1837a..bae6d0ffbb 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -26,13 +26,6 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new ScorePanel(score) // Todo: Temporary - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - State = PanelState.Expanded, - X = 30 - }, new Container { RelativeSizeAxes = Axes.Both, From 69d85ca3aeab18758ad644e1592d99a83fe35506 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 13:20:16 +0900 Subject: [PATCH 022/100] Add more cards to results screen test --- .../Visual/Ranking/TestSceneResultsScreen.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 125aa0a1e7..ea33aa62e3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -8,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens; @@ -113,6 +116,22 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var scores = new List(); + + for (int i = 0; i < 20; i++) + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + score.TotalScore += 10 - i; + scores.Add(score); + } + + scoresCallback?.Invoke(scores); + + return null; + } } private class UnrankedSoloResultsScreen : SoloResultsScreen From c31a05977d7b62a81e146de50ab374c8e2cca0d1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 16:50:45 +0900 Subject: [PATCH 023/100] Re-implement statistics as a click-in panel --- osu.Game/Screens/Ranking/ResultsScreen.cs | 109 +++++++----------- osu.Game/Screens/Ranking/ScorePanel.cs | 74 ++++++------ osu.Game/Screens/Ranking/ScorePanelList.cs | 67 ++++++----- .../Ranking/Statistics/StatisticsPanel.cs | 34 +++--- 4 files changed, 135 insertions(+), 149 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4ef012f6f2..927628a811 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -46,11 +44,9 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } - private Container scorePanelContainer; - private ResultsScrollContainer scrollContainer; - private Container expandedPanelProxyContainer; + private StatisticsPanel statisticsPanel; private Drawable bottomPanel; - private ScorePanelList panels; + private ScorePanelList scorePanelList; protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -63,13 +59,6 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - scorePanelContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }; - FillFlowContainer buttons; InternalChild = new GridContainer @@ -84,30 +73,26 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - scorePanelContainer, - scrollContainer = new ResultsScrollContainer + new OsuScrollContainer { - Child = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = screen_height, Children = new Drawable[] { - panels = new ScorePanelList(scorePanelContainer) + scorePanelList = new ScorePanelList { - RelativeSizeAxes = Axes.X, - Height = screen_height, - SelectedScore = { BindTarget = SelectedScore } + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = onExpandedPanelClicked }, - new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.X, - Height = screen_height, - } + statisticsPanel = new StatisticsPanel(Score) { RelativeSizeAxes = Axes.Both } } } }, - expandedPanelProxyContainer = new Container { RelativeSizeAxes = Axes.Both } } } }, @@ -155,7 +140,7 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - panels.AddScore(Score); + scorePanelList.AddScore(Score); if (player != null && allowRetry) { @@ -180,7 +165,7 @@ namespace osu.Game.Screens.Ranking var req = FetchScores(scores => Schedule(() => { foreach (var s in scores) - panels.AddScore(s); + scorePanelList.AddScore(s); })); if (req != null) @@ -194,21 +179,6 @@ namespace osu.Game.Screens.Ranking /// An responsible for the fetch operation. This will be queued and performed automatically. protected virtual APIRequest FetchScores(Action> scoresCallback) => null; - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - ScorePanel expandedPanel = scorePanelContainer.Single(p => p.State == PanelState.Expanded); - expandedPanel.Tracking = false; - expandedPanel.Anchor = Anchor.Centre; - expandedPanel.Origin = Anchor.Centre; - - scorePanelContainer.X = (float)Interpolation.Lerp(0, -DrawWidth / 2 + ScorePanel.EXPANDED_WIDTH / 2f, Math.Clamp(scrollContainer.Current / (screen_height * 0.8f), 0, 1)); - - if (expandedPanelProxyContainer.Count == 0) - expandedPanelProxyContainer.Add(expandedPanel.CreateProxy()); - } - public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -226,36 +196,39 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } - [Cached] - private class ResultsScrollContainer : OsuScrollContainer + private void onExpandedPanelClicked() { - public ResultsScrollContainer() + statisticsPanel.ToggleVisibility(); + + if (statisticsPanel.State.Value == Visibility.Hidden) { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; + foreach (var panel in scorePanelList.Panels) + { + if (panel.State == PanelState.Contracted) + panel.FadeIn(150); + else + { + panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p => + { + scorePanelList.HandleScroll = true; + p.Tracking = true; + }); + } + } } - - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + else { - if (!animated) + foreach (var panel in scorePanelList.Panels) { - // If the user is scrolling via mouse drag, follow the mouse 1:1. - base.OnUserScroll(value, false, distanceDecay); - return; - } + if (panel.State == PanelState.Contracted) + panel.FadeOut(150, Easing.OutQuint); + else + { + scorePanelList.HandleScroll = false; - float direction = Math.Sign(value - Target); - float target = Target + direction * screen_height; - - if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) - { - // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. - base.OnUserScroll(value, true, distanceDecay); - } - else - { - // Otherwise, scroll one screen in the target direction. - base.OnUserScroll(target, true, distanceDecay); + panel.Tracking = false; + panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 7ca96a9a58..31b2796c13 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -76,6 +76,18 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; + public Action PostExpandAction; + + /// + /// Whether this should track the position of the tracking component created via . + /// + public bool Tracking; + + /// + /// Whether this can enter into an state. + /// + public bool CanExpand = true; + public readonly ScoreInfo Score; private Container content; @@ -182,38 +194,18 @@ namespace osu.Game.Screens.Ranking } } - private bool tracking; - private Vector2 lastNonTrackingPosition; - - /// - /// Whether this should track the position of the tracking component created via . - /// - public bool Tracking - { - get => tracking; - set - { - if (tracking == value) - return; - - tracking = value; - - if (tracking) - lastNonTrackingPosition = Position; - else - Position = lastNonTrackingPosition; - } - } - protected override void Update() { base.Update(); if (Tracking && trackingComponent != null) - { - Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); - Position = topLeftPos - AnchorPosition + OriginPosition; - } + Position = GetTrackingPosition(); + } + + public Vector2 GetTrackingPosition() + { + Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); + return topLeftPos - AnchorPosition + OriginPosition; } private void updateState() @@ -270,10 +262,28 @@ namespace osu.Game.Screens.Ranking } } + public override Vector2 Size + { + get => base.Size; + set + { + base.Size = value; + + if (trackingComponent != null) + trackingComponent.Size = value; + } + } + protected override bool OnClick(ClickEvent e) { if (State == PanelState.Contracted) - State = PanelState.Expanded; + { + if (CanExpand) + State = PanelState.Expanded; + return true; + } + + PostExpandAction?.Invoke(); return true; } @@ -296,17 +306,13 @@ namespace osu.Game.Screens.Ranking Panel = panel; } - protected override void Update() - { - base.Update(); - Size = Panel.DrawSize; - } - // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. // So by forwarding input events, we remove the need to consider the order in which input is handled. protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); + + public override bool IsPresent => Panel.IsPresent; } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index d49085bc96..e332f462bb 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,9 +26,15 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + public Action PostExpandAction; + public readonly Bindable SelectedScore = new Bindable(); + public float CurrentScrollPosition => scroll.Current; + + public IReadOnlyList Panels => panels; private readonly Container panels; + private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -36,39 +42,27 @@ namespace osu.Game.Screens.Ranking /// /// Creates a new . /// - /// The target container in which s should reside. - /// s are set to track by default, but this allows - /// This should be placed _before_ the in the hierarchy. - /// - /// - public ScorePanelList(Container panelTarget = null) + public ScorePanelList() { RelativeSizeAxes = Axes.Both; InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - Child = flow = new Flow + HandleScroll = () => HandleScroll && expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(panel_spacing, 0), - AutoSizeAxes = Axes.Both, + panels = new Container { RelativeSizeAxes = Axes.Both }, + flow = new Flow + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, + }, } }; - - if (panelTarget == null) - { - // To prevent 1-frame sizing issues, the panel container is added _before_ the scroll + flow containers - AddInternal(panels = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = 1 - }); - } - else - panels = panelTarget; } protected override void LoadComplete() @@ -78,6 +72,25 @@ namespace osu.Game.Screens.Ranking SelectedScore.BindValueChanged(selectedScoreChanged, true); } + private bool handleScroll = true; + + public bool HandleScroll + { + get => handleScroll; + set + { + handleScroll = value; + + foreach (var p in panels) + p.CanExpand = value; + + scroll.ScrollbarVisible = value; + + if (!value) + scroll.ScrollTo(CurrentScrollPosition, false); + } + } + /// /// Adds a to this list. /// @@ -86,7 +99,8 @@ namespace osu.Game.Screens.Ranking { var panel = new ScorePanel(score) { - Tracking = true + Tracking = true, + PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { p.StateChanged += s => @@ -137,9 +151,6 @@ namespace osu.Game.Screens.Ranking var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue); expandedPanel = expandedTrackingComponent?.Panel; - // handle horizontal scroll only when not hovering the expanded panel. - scroll.HandleScroll = () => expandedPanel?.IsHovered != true; - if (expandedPanel == null) return; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index bae6d0ffbb..cc9007f527 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,17 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Scoring; using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - public class StatisticsPanel : CompositeDrawable + public class StatisticsPanel : VisibilityContainer { + protected override bool StartHidden => true; + public StatisticsPanel(ScoreInfo score) { // Todo: Not correct. @@ -19,27 +19,19 @@ namespace osu.Game.Screens.Ranking.Statistics FillFlowContainer statisticRows; - InternalChildren = new Drawable[] + InternalChild = new Container { - new Box + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") + Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, + Right = 50 }, - new Container + Child = statisticRows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, - Right = 50 - }, - Child = statisticRows = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - } + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 15), } }; @@ -55,5 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics }); } } + + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); } } From 6c8a24260bd4edd511b9284db59fe70e12f97347 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 17:06:05 +0900 Subject: [PATCH 024/100] Add padding --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 927628a811..4a7cb6679a 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -227,7 +227,7 @@ namespace osu.Game.Screens.Ranking scorePanelList.HandleScroll = false; panel.Tracking = false; - panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); + panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cc9007f527..733c855426 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -10,6 +10,8 @@ namespace osu.Game.Screens.Ranking.Statistics { public class StatisticsPanel : VisibilityContainer { + public const float SIDE_PADDING = 30; + protected override bool StartHidden => true; public StatisticsPanel(ScoreInfo score) @@ -24,8 +26,10 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, - Right = 50 + Left = ScorePanel.EXPANDED_WIDTH + SIDE_PADDING * 3, + Right = SIDE_PADDING, + Top = SIDE_PADDING, + Bottom = 50 // Approximate padding to the bottom of the score panel. }, Child = statisticRows = new FillFlowContainer { From 20db5b33abc952390f58a9110266f55f5377fc51 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:11:03 +0900 Subject: [PATCH 025/100] Rework score processor to provide more generic events --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuScoreProcessor.cs | 128 +++++++----------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 24 +++- .../Statistics/TimingDistributionGraph.cs | 45 ++++-- .../Ranking/TestSceneAccuracyHeatmap.cs | 9 +- .../Ranking/TestSceneStatisticsPanel.cs | 9 +- .../TestSceneTimingDistributionGraph.cs | 34 ++--- osu.Game/Scoring/ScoreInfo.cs | 4 +- 8 files changed, 126 insertions(+), 132 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 67a9bda1a9..c7003deed2 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -200,7 +201,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + Child = new TimingDistributionGraph(score.HitEvents.Cast().ToList()) { RelativeSizeAxes = Axes.Both } @@ -208,7 +209,7 @@ namespace osu.Game.Rulesets.Osu new StatisticContainer("Accuracy Heatmap") { RelativeSizeAxes = Axes.Both, - Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + Child = new Heatmap(score.Beatmap, score.HitEvents.Cast().ToList()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 97be372e37..9694367210 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,120 +1,52 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Diagnostics; -using osu.Game.Beatmaps; +using System.Linq; +using JetBrains.Annotations; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { - /// - /// The number of bins on each side of the timing distribution. - /// - private const int timing_distribution_bins = 25; - - /// - /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. - /// - private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; - - /// - /// The centre bin, with a timing distribution very close to/at 0. - /// - private const int timing_distribution_centre_bin_index = timing_distribution_bins; - - private TimingDistribution timingDistribution; - private readonly List hitOffsets = new List(); - - public override void ApplyBeatmap(IBeatmap beatmap) - { - var hitWindows = CreateHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - - timingDistribution = new TimingDistribution(total_timing_distribution_bins, hitWindows.WindowFor(hitWindows.LowestSuccessfulHitResult()) / timing_distribution_bins); - - base.ApplyBeatmap(beatmap); - } - - private OsuHitCircleJudgementResult lastCircleResult; + private readonly List hitEvents = new List(); + private HitObject lastHitObject; protected override void OnResultApplied(JudgementResult result) { base.OnResultApplied(result); - if (result.IsHit) - { - int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); - timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; - - addHitOffset(result); - } + hitEvents.Add(new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, (result as OsuHitCircleJudgementResult)?.HitPosition)); + lastHitObject = result.HitObject; } protected override void OnResultReverted(JudgementResult result) { base.OnResultReverted(result); - if (result.IsHit) - { - int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); - timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; - - removeHitOffset(result); - } - } - - private void addHitOffset(JudgementResult result) - { - if (!(result is OsuHitCircleJudgementResult circleResult)) - return; - - if (lastCircleResult == null) - { - lastCircleResult = circleResult; - return; - } - - if (circleResult.HitPosition != null) - { - Debug.Assert(circleResult.Radius != null); - hitOffsets.Add(new HitOffset(lastCircleResult.HitCircle.StackedEndPosition, circleResult.HitCircle.StackedEndPosition, circleResult.HitPosition.Value, circleResult.Radius.Value)); - } - - lastCircleResult = circleResult; - } - - private void removeHitOffset(JudgementResult result) - { - if (!(result is OsuHitCircleJudgementResult circleResult)) - return; - - if (hitOffsets.Count > 0 && circleResult.HitPosition != null) - hitOffsets.RemoveAt(hitOffsets.Count - 1); + hitEvents.RemoveAt(hitEvents.Count - 1); } protected override void Reset(bool storeResults) { base.Reset(storeResults); - timingDistribution.Bins.AsSpan().Clear(); - hitOffsets.Clear(); + hitEvents.Clear(); + lastHitObject = null; } public override void PopulateScore(ScoreInfo score) { base.PopulateScore(score); - score.ExtraStatistics["timing_distribution"] = timingDistribution; - score.ExtraStatistics["hit_offsets"] = hitOffsets; + score.HitEvents.AddRange(hitEvents.Select(e => e).Cast()); } protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) @@ -131,4 +63,42 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } + + public readonly struct HitEvent + { + /// + /// The time offset from the end of at which the event occurred. + /// + public readonly double TimeOffset; + + /// + /// The hit result. + /// + public readonly HitResult Result; + + /// + /// The on which the result occurred. + /// + public readonly HitObject HitObject; + + /// + /// The occurring prior to . + /// + [CanBeNull] + public readonly HitObject LastHitObject; + + /// + /// The player's cursor position, if available, at the time of the event. + /// + public readonly Vector2? CursorPosition; + + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? cursorPosition) + { + TimeOffset = timeOffset; + Result = result; + HitObject = hitObject; + LastHitObject = lastHitObject; + CursorPosition = cursorPosition; + } + } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 51508a5e8b..95cfc5b768 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -27,14 +29,16 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; private const float point_size = 4; - private readonly IReadOnlyList offsets; private Container allPoints; + private readonly BeatmapInfo beatmap; + private readonly IReadOnlyList hitEvents; private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); - public Heatmap(IReadOnlyList offsets) + public Heatmap(BeatmapInfo beatmap, IReadOnlyList hitEvents) { - this.offsets = offsets; + this.beatmap = beatmap; + this.hitEvents = hitEvents; AddLayout(sizeLayout); } @@ -153,10 +157,18 @@ namespace osu.Game.Rulesets.Osu.Statistics } } - if (offsets?.Count > 0) + if (hitEvents.Count > 0) { - foreach (var o in offsets) - AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + // Todo: This should probably not be done like this. + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + + foreach (var e in hitEvents) + { + if (e.LastHitObject == null || e.CursorPosition == null) + continue; + + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); + } } sizeLayout.Validate(); diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 1f9f38bf3b..b319cc5aa9 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -15,29 +17,52 @@ namespace osu.Game.Rulesets.Osu.Statistics { public class TimingDistributionGraph : CompositeDrawable { + /// + /// The number of bins on each side of the timing distribution. + /// + private const int timing_distribution_bins = 25; + + /// + /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. + /// + private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; + + /// + /// The centre bin, with a timing distribution very close to/at 0. + /// + private const int timing_distribution_centre_bin_index = timing_distribution_bins; + /// /// The number of data points shown on the axis below the graph. /// private const float axis_points = 5; - private readonly TimingDistribution distribution; + private readonly List hitEvents; - public TimingDistributionGraph(TimingDistribution distribution) + public TimingDistributionGraph(List hitEvents) { - this.distribution = distribution; + this.hitEvents = hitEvents; } [BackgroundDependencyLoader] private void load() { - if (distribution?.Bins == null || distribution.Bins.Length == 0) + if (hitEvents.Count == 0) return; - int maxCount = distribution.Bins.Max(); + int[] bins = new int[total_timing_distribution_bins]; + double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - var bars = new Drawable[distribution.Bins.Length]; + foreach (var e in hitEvents) + { + int binOffset = (int)(e.TimeOffset / binSize); + bins[timing_distribution_centre_bin_index + binOffset]++; + } + + int maxCount = bins.Max(); + var bars = new Drawable[total_timing_distribution_bins]; for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + bars[i] = new Bar { Height = (float)bins[i] / maxCount }; Container axisFlow; @@ -71,10 +96,8 @@ namespace osu.Game.Rulesets.Osu.Statistics } }; - // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. - // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - int sideBins = (distribution.Bins.Length - 1) / 2; - double maxValue = sideBins * distribution.BinSize; + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; double axisValueStep = maxValue / axis_points; axisFlow.Add(new OsuSpriteText diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 53c8e56f53..ba6a0e42c2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -8,8 +8,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new TestHeatmap(new List()) + heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -73,8 +76,8 @@ namespace osu.Game.Tests.Visual.Ranking private class TestHeatmap : Heatmap { - public TestHeatmap(IReadOnlyList offsets) - : base(offsets) + public TestHeatmap(BeatmapInfo beatmap, List events) + : base(beatmap, events) { } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c02be9ab5d..faabdf2cb6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -17,11 +16,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - ExtraStatistics = - { - ["timing_distribution"] = TestSceneTimingDistributionGraph.CreateNormalDistribution(), - ["hit_offsets"] = new List() - } + HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents().Cast().ToList(), }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 2249655093..178d6d95b5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -22,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(CreateNormalDistribution()) + new TimingDistributionGraph(CreateDistributedHitEvents()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,34 +34,19 @@ namespace osu.Game.Tests.Visual.Ranking }; } - public static TimingDistribution CreateNormalDistribution() + public static List CreateDistributedHitEvents() { - var distribution = new TimingDistribution(51, 5); + var hitEvents = new List(); - // We create an approximately-normal distribution of 51 elements by using the 13th binomial row (14 initial elements) and subdividing the inner values twice. - var row = new List { 1 }; - for (int i = 0; i < 13; i++) - row.Add(row[i] * (13 - i) / (i + 1)); - - // Each subdivision yields 2n-1 total elements, so first subdivision will contain 27 elements, and the second will contain 53 elements. - for (int div = 0; div < 2; div++) + for (int i = 0; i < 50; i++) { - var newRow = new List { 1 }; + int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2)); - for (int i = 0; i < row.Count - 1; i++) - { - newRow.Add((row[i] + row[i + 1]) / 2); - newRow.Add(row[i + 1]); - } - - row = newRow; + for (int j = 0; j < count; j++) + hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); } - // After the subdivisions take place, we're left with 53 values which we use the inner 51 of. - for (int i = 1; i < row.Count - 1; i++) - distribution.Bins[i - 1] = row[i]; - - return distribution; + return hitEvents; } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 38b37afc55..6fc5892b3c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -166,7 +166,9 @@ namespace osu.Game.Scoring } } - public Dictionary ExtraStatistics = new Dictionary(); + [NotMapped] + [JsonIgnore] + public List HitEvents = new List(); [JsonIgnore] public List Files { get; set; } From ecdfcb1955f4929bc11fe1e3d1e8e1ddadfbd119 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:21:30 +0900 Subject: [PATCH 026/100] Display placeholder if no statistics available --- .../Ranking/TestSceneStatisticsPanel.cs | 17 +++++- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 +- .../Ranking/Statistics/StatisticsPanel.cs | 61 +++++++++++++------ 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index faabdf2cb6..cc3415a530 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -3,6 +3,8 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -12,7 +14,7 @@ namespace osu.Game.Tests.Visual.Ranking public class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScore() + public void TestScoreWithStatistics() { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { @@ -22,9 +24,20 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(score); } + [Test] + public void TestScoreWithoutStatistics() + { + loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel(score); + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score } + }; }); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4a7cb6679a..c02a120a73 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -89,7 +89,11 @@ namespace osu.Game.Screens.Ranking SelectedScore = { BindTarget = SelectedScore }, PostExpandAction = onExpandedPanelClicked }, - statisticsPanel = new StatisticsPanel(Score) { RelativeSizeAxes = Axes.Both } + statisticsPanel = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore } + } } } }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 733c855426..28a8bc460e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osuTK; @@ -12,15 +15,14 @@ namespace osu.Game.Screens.Ranking.Statistics { public const float SIDE_PADDING = 30; + public readonly Bindable Score = new Bindable(); + protected override bool StartHidden => true; - public StatisticsPanel(ScoreInfo score) + private readonly Container content; + + public StatisticsPanel() { - // Todo: Not correct. - RelativeSizeAxes = Axes.Both; - - FillFlowContainer statisticRows; - InternalChild = new Container { RelativeSizeAxes = Axes.Both, @@ -31,24 +33,47 @@ namespace osu.Game.Screens.Ranking.Statistics Top = SIDE_PADDING, Bottom = 50 // Approximate padding to the bottom of the score panel. }, - Child = statisticRows = new FillFlowContainer + Child = content = new Container { RelativeSizeAxes = Axes.Both }, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + Score.BindValueChanged(populateStatistics, true); + } + + private void populateStatistics(ValueChangedEvent score) + { + foreach (var child in content) + child.FadeOut(150).Expire(); + + var newScore = score.NewValue; + + if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) + content.Add(new MessagePlaceholder("Score has no statistics :(")); + else + { + var rows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), - } - }; + }; - foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) - { - statisticRows.Add(new GridContainer + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatistics(newScore)) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { s.Content }, - ColumnDimensions = s.ColumnDimensions, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); + rows.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { row.Content }, + ColumnDimensions = row.ColumnDimensions, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } + + content.Add(rows); } } From 53f507f51af7adc2e48200281fc8ff1598489142 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:27:10 +0900 Subject: [PATCH 027/100] Fade background --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c02a120a73..5073adcc50 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -219,6 +219,8 @@ namespace osu.Game.Screens.Ranking }); } } + + Background.FadeTo(0.5f, 150); } else { @@ -234,6 +236,8 @@ namespace osu.Game.Screens.Ranking panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } + + Background.FadeTo(0.1f, 150); } } } From 85a0f78600e97866c1726eefeed64113fef5d76c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:27:27 +0900 Subject: [PATCH 028/100] Hide statistics panel on first exit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5073adcc50..de1939352f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = onExpandedPanelClicked + PostExpandAction = () => statisticsPanel.ToggleVisibility() }, statisticsPanel = new StatisticsPanel { @@ -174,6 +174,8 @@ namespace osu.Game.Screens.Ranking if (req != null) api.Queue(req); + + statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } /// @@ -195,17 +197,23 @@ namespace osu.Game.Screens.Ranking public override bool OnExiting(IScreen next) { + if (statisticsPanel.State.Value == Visibility.Visible) + { + statisticsPanel.Hide(); + return true; + } + Background.FadeTo(1, 250); return base.OnExiting(next); } - private void onExpandedPanelClicked() + private void onStatisticsStateChanged(ValueChangedEvent state) { - statisticsPanel.ToggleVisibility(); - - if (statisticsPanel.State.Value == Visibility.Hidden) + if (state.NewValue == Visibility.Hidden) { + Background.FadeTo(0.5f, 150); + foreach (var panel in scorePanelList.Panels) { if (panel.State == PanelState.Contracted) @@ -219,11 +227,11 @@ namespace osu.Game.Screens.Ranking }); } } - - Background.FadeTo(0.5f, 150); } else { + Background.FadeTo(0.1f, 150); + foreach (var panel in scorePanelList.Panels) { if (panel.State == PanelState.Contracted) @@ -236,8 +244,6 @@ namespace osu.Game.Screens.Ranking panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } - - Background.FadeTo(0.1f, 150); } } } From 5530e2a1dbaa413a4383348ca7b2cf042d3c1c60 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 15:35:39 +0900 Subject: [PATCH 029/100] Add test for delayed score fetch --- .../Visual/Ranking/TestSceneResultsScreen.cs | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ea33aa62e3..9d3c22d87c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; @@ -16,6 +18,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { @@ -44,7 +47,7 @@ namespace osu.Game.Tests.Visual.Ranking private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); [Test] - public void ResultsWithoutPlayer() + public void TestResultsWithoutPlayer() { TestResultsScreen screen = null; OsuScreenStack stack; @@ -63,7 +66,7 @@ namespace osu.Game.Tests.Visual.Ranking } [Test] - public void ResultsWithPlayer() + public void TestResultsWithPlayer() { TestResultsScreen screen = null; @@ -73,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking } [Test] - public void ResultsForUnranked() + public void TestResultsForUnranked() { UnrankedSoloResultsScreen screen = null; @@ -82,6 +85,24 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestFetchScoresAfterShowingStatistics() + { + DelayedFetchResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000))); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for fetch", () => screen.FetchCompleted); + AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -134,6 +155,42 @@ namespace osu.Game.Tests.Visual.Ranking } } + private class DelayedFetchResultsScreen : TestResultsScreen + { + public bool FetchCompleted { get; private set; } + + private readonly double delay; + + public DelayedFetchResultsScreen(ScoreInfo score, double delay) + : base(score) + { + this.delay = delay; + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + + var scores = new List(); + + for (int i = 0; i < 20; i++) + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + score.TotalScore += 10 - i; + scores.Add(score); + } + + scoresCallback?.Invoke(scores); + + Schedule(() => FetchCompleted = true); + }); + + return null; + } + } + private class UnrankedSoloResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; From ec16b0fc5a3888c65b198da0640b563accba5803 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 17:28:35 +0900 Subject: [PATCH 030/100] Rework score panel tracking to fix visual edge cases --- .../Visual/Ranking/TestSceneScorePanel.cs | 38 -------- osu.Game/Screens/Ranking/ResultsScreen.cs | 97 +++++++++++++------ osu.Game/Screens/Ranking/ScorePanel.cs | 45 ++------- osu.Game/Screens/Ranking/ScorePanelList.cs | 79 +++++++++------ .../Ranking/ScorePanelTrackingContainer.cs | 35 +++++++ 5 files changed, 155 insertions(+), 139 deletions(-) create mode 100644 osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 1c5087ee94..250fdc5ebd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -102,39 +101,6 @@ namespace osu.Game.Tests.Visual.Ranking AddWaitStep("wait for transition", 10); } - [Test] - public void TestSceneTrackingScorePanel() - { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; - - addPanelStep(score, PanelState.Contracted); - - AddStep("enable tracking", () => - { - panel.Anchor = Anchor.CentreLeft; - panel.Origin = Anchor.CentreLeft; - panel.Tracking = true; - - Add(panel.CreateTrackingComponent().With(d => - { - d.Anchor = Anchor.Centre; - d.Origin = Anchor.Centre; - })); - }); - - assertTracking(true); - - AddStep("expand panel", () => panel.State = PanelState.Expanded); - AddWaitStep("wait for transition", 2); - assertTracking(true); - - AddStep("stop tracking", () => panel.Tracking = false); - assertTracking(false); - - AddStep("start tracking", () => panel.Tracking = true); - assertTracking(true); - } - private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { Child = panel = new ScorePanel(score) @@ -144,9 +110,5 @@ namespace osu.Game.Tests.Visual.Ranking State = state }; }); - - private void assertTracking(bool tracking) => AddAssert($"{(tracking ? "is" : "is not")} tracking", () => - Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.TopLeft, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.TopLeft) == tracking - && Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.BottomRight, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.BottomRight) == tracking); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index de1939352f..133efd6e7b 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -47,6 +48,7 @@ namespace osu.Game.Screens.Ranking private StatisticsPanel statisticsPanel; private Drawable bottomPanel; private ScorePanelList scorePanelList; + private Container detachedPanelContainer; protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -89,11 +91,15 @@ namespace osu.Game.Screens.Ranking SelectedScore = { BindTarget = SelectedScore }, PostExpandAction = () => statisticsPanel.ToggleVisibility() }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, statisticsPanel = new StatisticsPanel { RelativeSizeAxes = Axes.Both, Score = { BindTarget = SelectedScore } - } + }, } } }, @@ -169,7 +175,7 @@ namespace osu.Game.Screens.Ranking var req = FetchScores(scores => Schedule(() => { foreach (var s in scores) - scorePanelList.AddScore(s); + addScore(s); })); if (req != null) @@ -208,42 +214,71 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + private void addScore(ScoreInfo score) + { + var panel = scorePanelList.AddScore(score); + + if (detachedPanel != null) + panel.Alpha = 0; + } + + private ScorePanel detachedPanel; + private void onStatisticsStateChanged(ValueChangedEvent state) { - if (state.NewValue == Visibility.Hidden) + if (state.NewValue == Visibility.Visible) { - Background.FadeTo(0.5f, 150); + // Detach the panel in its original location, and move into the desired location in the local container. + var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value); + var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft; - foreach (var panel in scorePanelList.Panels) - { - if (panel.State == PanelState.Contracted) - panel.FadeIn(150); - else - { - panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p => - { - scorePanelList.HandleScroll = true; - p.Tracking = true; - }); - } - } - } - else - { + // Detach and move into the local container. + scorePanelList.Detach(expandedPanel); + detachedPanelContainer.Add(expandedPanel); + + // Move into its original location in the local container. + var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); + expandedPanel.MoveTo(origLocation); + expandedPanel.MoveToX(origLocation.X); + + // Move into the final location. + expandedPanel.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + + // Hide contracted panels. + foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) + contracted.FadeOut(150, Easing.OutQuint); + scorePanelList.HandleInput = false; + + // Dim background. Background.FadeTo(0.1f, 150); - foreach (var panel in scorePanelList.Panels) - { - if (panel.State == PanelState.Contracted) - panel.FadeOut(150, Easing.OutQuint); - else - { - scorePanelList.HandleScroll = false; + detachedPanel = expandedPanel; + } + else if (detachedPanel != null) + { + var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft; - panel.Tracking = false; - panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); - } - } + // Remove from the local container and re-attach. + detachedPanelContainer.Remove(detachedPanel); + scorePanelList.Attach(detachedPanel); + + // Move into its original location in the attached container. + var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos); + detachedPanel.MoveTo(origLocation); + detachedPanel.MoveToX(origLocation.X); + + // Move into the final location. + detachedPanel.MoveToX(0, 150, Easing.OutQuint); + + // Show contracted panels. + foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) + contracted.FadeIn(150, Easing.OutQuint); + scorePanelList.HandleInput = true; + + // Un-dim background. + Background.FadeTo(0.5f, 150); + + detachedPanel = null; } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 31b2796c13..257279bdc9 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -78,11 +78,6 @@ namespace osu.Game.Screens.Ranking public event Action StateChanged; public Action PostExpandAction; - /// - /// Whether this should track the position of the tracking component created via . - /// - public bool Tracking; - /// /// Whether this can enter into an state. /// @@ -194,20 +189,6 @@ namespace osu.Game.Screens.Ranking } } - protected override void Update() - { - base.Update(); - - if (Tracking && trackingComponent != null) - Position = GetTrackingPosition(); - } - - public Vector2 GetTrackingPosition() - { - Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); - return topLeftPos - AnchorPosition + OriginPosition; - } - private void updateState() { topLayerContent?.FadeOut(content_fade_duration).Expire(); @@ -269,8 +250,8 @@ namespace osu.Game.Screens.Ranking { base.Size = value; - if (trackingComponent != null) - trackingComponent.Size = value; + if (trackingContainer != null) + trackingContainer.Size = value; } } @@ -293,26 +274,14 @@ namespace osu.Game.Screens.Ranking || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); - private TrackingComponent trackingComponent; + private ScorePanelTrackingContainer trackingContainer; - public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this); - - public class TrackingComponent : Drawable + public ScorePanelTrackingContainer CreateTrackingContainer() { - public readonly ScorePanel Panel; + if (trackingContainer != null) + throw new InvalidOperationException("A score panel container has already been created."); - public TrackingComponent(ScorePanel panel) - { - Panel = panel; - } - - // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. - // So by forwarding input events, we remove the need to consider the order in which input is handled. - protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); - - public override bool IsPresent => Panel.IsPresent; + return trackingContainer = new ScorePanelTrackingContainer(this); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index e332f462bb..32903860ec 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -32,9 +32,6 @@ namespace osu.Game.Screens.Ranking public float CurrentScrollPosition => scroll.Current; - public IReadOnlyList Panels => panels; - private readonly Container panels; - private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -49,10 +46,9 @@ namespace osu.Game.Screens.Ranking InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - HandleScroll = () => HandleScroll && expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. + HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. Children = new Drawable[] { - panels = new Container { RelativeSizeAxes = Axes.Both }, flow = new Flow { Anchor = Anchor.Centre, @@ -72,34 +68,14 @@ namespace osu.Game.Screens.Ranking SelectedScore.BindValueChanged(selectedScoreChanged, true); } - private bool handleScroll = true; - - public bool HandleScroll - { - get => handleScroll; - set - { - handleScroll = value; - - foreach (var p in panels) - p.CanExpand = value; - - scroll.ScrollbarVisible = value; - - if (!value) - scroll.ScrollTo(CurrentScrollPosition, false); - } - } - /// /// Adds a to this list. /// /// The to add. - public void AddScore(ScoreInfo score) + public ScorePanel AddScore(ScoreInfo score) { var panel = new ScorePanel(score) { - Tracking = true, PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { @@ -110,8 +86,7 @@ namespace osu.Game.Screens.Ranking }; }); - panels.Add(panel); - flow.Add(panel.CreateTrackingComponent().With(d => + flow.Add(panel.CreateTrackingContainer().With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; @@ -132,6 +107,8 @@ namespace osu.Game.Screens.Ranking scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; } } + + return panel; } /// @@ -187,15 +164,53 @@ namespace osu.Game.Screens.Ranking flow.Padding = new MarginPadding { Horizontal = offset }; } - private class Flow : FillFlowContainer + private bool handleInput = true; + + public bool HandleInput + { + get => handleInput; + set + { + handleInput = value; + scroll.ScrollbarVisible = value; + } + } + + public override bool PropagatePositionalInputSubTree => HandleInput && base.PropagatePositionalInputSubTree; + + public override bool PropagateNonPositionalInputSubTree => HandleInput && base.PropagateNonPositionalInputSubTree; + + public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); + + public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel; + + public void Detach(ScorePanel panel) + { + var container = flow.FirstOrDefault(t => t.Panel == panel); + if (container == null) + throw new InvalidOperationException("Panel is not contained by the score panel list."); + + container.Detach(); + } + + public void Attach(ScorePanel panel) + { + var container = flow.FirstOrDefault(t => t.Panel == panel); + if (container == null) + throw new InvalidOperationException("Panel is not contained by the score panel list."); + + container.Attach(); + } + + private class Flow : FillFlowContainer { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); - private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() - .OrderByDescending(s => s.Panel.Score.TotalScore) - .ThenBy(s => s.Panel.Score.OnlineScoreID); + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Panel.Score.TotalScore) + .ThenBy(s => s.Panel.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs new file mode 100644 index 0000000000..f6f26d0f8a --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanelTrackingContainer : CompositeDrawable + { + public readonly ScorePanel Panel; + + public ScorePanelTrackingContainer(ScorePanel panel) + { + Panel = panel; + Attach(); + } + + public void Detach() + { + if (InternalChildren.Count == 0) + throw new InvalidOperationException("Score panel container is not attached."); + + RemoveInternal(Panel); + } + + public void Attach() + { + if (InternalChildren.Count > 0) + throw new InvalidOperationException("Score panel container is already attached."); + + AddInternal(Panel); + } + } +} From 55196efe6e4ae03efe09e7dbc2b79d7d90f8e4c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 18:02:54 +0900 Subject: [PATCH 031/100] Fix panel depth ordering --- osu.Game/Screens/Ranking/ScorePanelList.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 32903860ec..8f9064c2d1 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -211,6 +211,22 @@ namespace osu.Game.Screens.Ranking private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(s => s.Panel.Score.TotalScore) .ThenBy(s => s.Panel.Score.OnlineScoreID); + + protected override int Compare(Drawable x, Drawable y) + { + var tX = (ScorePanelTrackingContainer)x; + var tY = (ScorePanelTrackingContainer)y; + + int result = tY.Panel.Score.TotalScore.CompareTo(tX.Panel.Score.TotalScore); + + if (result != 0) + return result; + + if (tX.Panel.Score.OnlineScoreID == null || tY.Panel.Score.OnlineScoreID == null) + return base.Compare(x, y); + + return tX.Panel.Score.OnlineScoreID.Value.CompareTo(tY.Panel.Score.OnlineScoreID.Value); + } } private class Scroll : OsuScrollContainer From c9ad3192b02ae4a9a2cc4d6b19adb9b84d54d4ef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 18:02:57 +0900 Subject: [PATCH 032/100] Add more tests --- .../Visual/Ranking/TestSceneResultsScreen.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 9d3c22d87c..ac364b5233 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; @@ -18,6 +19,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Statistics; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -85,6 +87,73 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestShowHideStatistics() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddUntilStep("expanded panel at the left of the screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150; + }); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddUntilStep("expanded panel in centre of screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1); + }); + } + + [Test] + public void TestShowStatisticsAndClickOtherPanel() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + ScorePanel expandedPanel = null; + ScorePanel contractedPanel = null; + + AddStep("click expanded panel then contracted panel", () => + { + expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + + contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X); + InputManager.MoveMouseTo(contractedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddAssert("contracted panel still contracted", () => contractedPanel.State == PanelState.Contracted); + AddAssert("expanded panel still expanded", () => expandedPanel.State == PanelState.Expanded); + } + [Test] public void TestFetchScoresAfterShowingStatistics() { From cae3a5f447e166b57bfed30a52e4a51964e4e2a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:08:36 +0900 Subject: [PATCH 033/100] Rework heatmap for more consistent performance --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 94 +++++++++---------- .../Ranking/TestSceneAccuracyHeatmap.cs | 55 +++++++---- 2 files changed, 78 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 95cfc5b768..8ebc8e9001 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -26,10 +26,15 @@ namespace osu.Game.Rulesets.Osu.Statistics /// private const float inner_portion = 0.8f; - private const float rotation = 45; - private const float point_size = 4; + /// + /// Number of rows/columns of points. + /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// + private const int points_per_dimension = 32; - private Container allPoints; + private const float rotation = 45; + + private GridContainer pointGrid; private readonly BeatmapInfo beatmap; private readonly IReadOnlyList hitEvents; @@ -111,52 +116,39 @@ namespace osu.Game.Rulesets.Osu.Statistics } } }, - allPoints = new Container + pointGrid = new GridContainer { RelativeSizeAxes = Axes.Both } } }; - } - protected override void Update() - { - base.Update(); - validateHitPoints(); - } + Vector2 centre = new Vector2(points_per_dimension) / 2; + float innerRadius = centre.X * inner_portion; - private void validateHitPoints() - { - if (sizeLayout.IsValid) - return; + Drawable[][] points = new Drawable[points_per_dimension][]; - allPoints.Clear(); - - // Since the content is fit, both dimensions should have the same size. - float size = allPoints.DrawSize.X; - - Vector2 centre = new Vector2(size / 2); - int rows = (int)Math.Ceiling(size / point_size); - int cols = (int)Math.Ceiling(size / point_size); - - for (int r = 0; r < rows; r++) + for (int r = 0; r < points_per_dimension; r++) { - for (int c = 0; c < cols; c++) + points[r] = new Drawable[points_per_dimension]; + + for (int c = 0; c < points_per_dimension; c++) { - Vector2 pos = new Vector2(c * point_size, r * point_size); - HitPointType pointType = HitPointType.Hit; + HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius + ? HitPointType.Hit + : HitPointType.Miss; - if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - pointType = HitPointType.Miss; - - allPoints.Add(new HitPoint(pos, pointType) + var point = new HitPoint(pointType) { - Size = new Vector2(point_size), Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) - }); + }; + + points[r][c] = point; } } + pointGrid.Content = points; + if (hitEvents.Count > 0) { // Todo: This should probably not be done like this. @@ -170,41 +162,39 @@ namespace osu.Game.Rulesets.Osu.Statistics AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); } } - - sizeLayout.Validate(); } protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { - if (allPoints.Count == 0) + if (pointGrid.Content.Length == 0) return; double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. - float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - // Since the content is fit, both dimensions should have the same size. - float size = allPoints.DrawSize.X; + // Convert the above into the local search space. + Vector2 localCentre = new Vector2(points_per_dimension) / 2; + float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. + double localAngle = finalAngle + 3 * Math.PI / 4; // The angle inside the heatmap on which the closest point lies. + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. double minDist = double.PositiveInfinity; HitPoint point = null; - foreach (var p in allPoints) + for (int r = 0; r < points_per_dimension; r++) { - Vector2 localCentre = new Vector2(size / 2); - float localRadius = localCentre.X * inner_portion * normalisedDistance; - double localAngle = finalAngle + 3 * Math.PI / 4; - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); - - float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); - - if (dist < minDist) + for (int c = 0; c < points_per_dimension; c++) { - minDist = dist; - point = p; + float dist = Vector2.Distance(new Vector2(c, r), localPoint); + + if (dist < minDist) + { + minDist = dist; + point = (HitPoint)pointGrid.Content[r][c]; + } } } @@ -216,11 +206,11 @@ namespace osu.Game.Rulesets.Osu.Statistics { private readonly HitPointType pointType; - public HitPoint(Vector2 position, HitPointType pointType) + public HitPoint(HitPointType pointType) { this.pointType = pointType; - Position = position; + RelativeSizeAxes = Axes.Both; Alpha = 0; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index ba6a0e42c2..52cc41fbd8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; @@ -20,13 +22,18 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene { - private readonly Box background; - private readonly Drawable object1; - private readonly Drawable object2; - private readonly TestHeatmap heatmap; + private Box background; + private Drawable object1; + private Drawable object2; + private TestHeatmap heatmap; + private ScheduledDelegate automaticAdditionDelegate; - public TestSceneAccuracyHeatmap() + [SetUp] + public void Setup() => Schedule(() => { + automaticAdditionDelegate?.Cancel(); + automaticAdditionDelegate = null; + Children = new[] { background = new Box @@ -41,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking }, object2 = new BorderCircle { - Position = new Vector2(500, 300), + Position = new Vector2(100, 300), }, heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) { @@ -50,22 +57,32 @@ namespace osu.Game.Tests.Visual.Ranking Size = new Vector2(130) } }; + }); + + [Test] + public void TestManyHitPointsAutomatic() + { + AddStep("add scheduled delegate", () => + { + automaticAdditionDelegate = Scheduler.AddDelayed(() => + { + var randomPos = new Vector2( + RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), + RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); + + // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). + heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); + }, 1, true); + }); + + AddWaitStep("wait for some hit points", 10); } - protected override void LoadComplete() + [Test] + public void TestManualPlacement() { - base.LoadComplete(); - - Scheduler.AddDelayed(() => - { - var randomPos = new Vector2( - RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), - RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); - - // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). - heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); - InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); - }, 1, true); + AddStep("return user input", () => InputManager.UseParentInput = true); } protected override bool OnMouseDown(MouseDownEvent e) From d3e4e6325884a2ac753d3c0c2c2601accb7a4d2f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:12:48 +0900 Subject: [PATCH 034/100] Remove unnecessary class --- .../Scoring/TimingDistribution.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs deleted file mode 100644 index 46f259f3d8..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Osu.Scoring -{ - public class TimingDistribution - { - public readonly int[] Bins; - public readonly double BinSize; - - public TimingDistribution(int binCount, double binSize) - { - Bins = new int[binCount]; - BinSize = binSize; - } - } -} From a3ff25177ad782e562732315a74be1557ca19ffc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:12:55 +0900 Subject: [PATCH 035/100] Asyncify statistics load --- .../Ranking/Statistics/StatisticsPanel.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 28a8bc460e..acaf91246d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osuTK; @@ -20,6 +22,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool StartHidden => true; private readonly Container content; + private readonly LoadingSpinner spinner; public StatisticsPanel() { @@ -33,7 +36,11 @@ namespace osu.Game.Screens.Ranking.Statistics Top = SIDE_PADDING, Bottom = 50 // Approximate padding to the bottom of the score panel. }, - Child = content = new Container { RelativeSizeAxes = Axes.Both }, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + spinner = new LoadingSpinner() + } }; } @@ -43,8 +50,12 @@ namespace osu.Game.Screens.Ranking.Statistics Score.BindValueChanged(populateStatistics, true); } + private CancellationTokenSource loadCancellation; + private void populateStatistics(ValueChangedEvent score) { + loadCancellation?.Cancel(); + foreach (var child in content) child.FadeOut(150).Expire(); @@ -54,6 +65,8 @@ namespace osu.Game.Screens.Ranking.Statistics content.Add(new MessagePlaceholder("Score has no statistics :(")); else { + spinner.Show(); + var rows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, @@ -73,7 +86,14 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - content.Add(rows); + LoadComponentAsync(rows, d => + { + if (Score.Value != newScore) + return; + + spinner.Hide(); + content.Add(d); + }, (loadCancellation = new CancellationTokenSource()).Token); } } From 8c9506197d30b1635bb51541959978221d7f0d94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:41:36 +0900 Subject: [PATCH 036/100] Increase the number of bins in the timing distribution --- osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index b319cc5aa9..30d25f581f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Statistics /// /// The number of bins on each side of the timing distribution. /// - private const int timing_distribution_bins = 25; + private const int timing_distribution_bins = 50; /// /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. From ef56225d9adfda9bd45038746cd03edfb244a7b0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:43:46 +0900 Subject: [PATCH 037/100] Rename CursorPosition -< PositionOffset --- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 8 ++++---- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 9694367210..0a9ce83912 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -88,17 +88,17 @@ namespace osu.Game.Rulesets.Osu.Scoring public readonly HitObject LastHitObject; /// - /// The player's cursor position, if available, at the time of the event. + /// The player's position offset, if available, at the time of the event. /// - public readonly Vector2? CursorPosition; + public readonly Vector2? PositionOffset; - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? cursorPosition) + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? positionOffset) { TimeOffset = timeOffset; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; - CursorPosition = cursorPosition; + PositionOffset = positionOffset; } } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 8ebc8e9001..b648dd5e47 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -156,10 +156,10 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var e in hitEvents) { - if (e.LastHitObject == null || e.CursorPosition == null) + if (e.LastHitObject == null || e.PositionOffset == null) continue; - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); } } } From eab00ec9d9644f32e72268c819fad8cf5801e17c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:58:35 +0900 Subject: [PATCH 038/100] Move hit events to the ScoreProcessor --- .../Judgements/OsuHitCircleJudgementResult.cs | 9 ++- .../Objects/Drawables/DrawableHitCircle.cs | 4 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuScoreProcessor.cs | 76 ------------------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 36 ++++----- .../Statistics/TimingDistributionGraph.cs | 15 ++-- .../Ranking/TestSceneAccuracyHeatmap.cs | 10 +-- .../Ranking/TestSceneStatisticsPanel.cs | 3 +- .../TestSceneTimingDistributionGraph.cs | 4 +- osu.Game/Rulesets/Scoring/HitEvent.cs | 48 ++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 18 +++++ osu.Game/Scoring/ScoreInfo.cs | 2 +- 12 files changed, 106 insertions(+), 124 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/HitEvent.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs index 103d02958d..9b33e746b3 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -10,10 +10,15 @@ namespace osu.Game.Rulesets.Osu.Judgements { public class OsuHitCircleJudgementResult : OsuJudgementResult { + /// + /// The . + /// public HitCircle HitCircle => (HitCircle)HitObject; - public Vector2? HitPosition; - public float? Radius; + /// + /// The position of the player's cursor when was hit. + /// + public Vector2? CursorPositionAtHit; public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement) : base(hitObject, judgement) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 2f86400b25..854fc4c91c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -142,11 +142,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { var circleResult = (OsuHitCircleJudgementResult)r; + // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. if (result != HitResult.Miss) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - circleResult.HitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); - circleResult.Radius = (float)HitObject.Radius; + circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); } circleResult.Type = result; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c7003deed2..45980cb3d5 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,7 +29,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; -using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -201,7 +200,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph(score.HitEvents.Cast().ToList()) + Child = new TimingDistributionGraph(score) { RelativeSizeAxes = Axes.Both } @@ -209,7 +208,7 @@ namespace osu.Game.Rulesets.Osu new StatisticContainer("Accuracy Heatmap") { RelativeSizeAxes = Axes.Both, - Child = new Heatmap(score.Beatmap, score.HitEvents.Cast().ToList()) + Child = new Heatmap(score) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 0a9ce83912..231a24cac5 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,54 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osuTK; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { - private readonly List hitEvents = new List(); - private HitObject lastHitObject; - - protected override void OnResultApplied(JudgementResult result) - { - base.OnResultApplied(result); - - hitEvents.Add(new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, (result as OsuHitCircleJudgementResult)?.HitPosition)); - lastHitObject = result.HitObject; - } - - protected override void OnResultReverted(JudgementResult result) - { - base.OnResultReverted(result); - - hitEvents.RemoveAt(hitEvents.Count - 1); - } - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - hitEvents.Clear(); - lastHitObject = null; - } - - public override void PopulateScore(ScoreInfo score) - { - base.PopulateScore(score); - - score.HitEvents.AddRange(hitEvents.Select(e => e).Cast()); - } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) { switch (hitObject) @@ -63,42 +25,4 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } - - public readonly struct HitEvent - { - /// - /// The time offset from the end of at which the event occurred. - /// - public readonly double TimeOffset; - - /// - /// The hit result. - /// - public readonly HitResult Result; - - /// - /// The on which the result occurred. - /// - public readonly HitObject HitObject; - - /// - /// The occurring prior to . - /// - [CanBeNull] - public readonly HitObject LastHitObject; - - /// - /// The player's position offset, if available, at the time of the event. - /// - public readonly Vector2? PositionOffset; - - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? positionOffset) - { - TimeOffset = timeOffset; - Result = result; - HitObject = hitObject; - LastHitObject = lastHitObject; - PositionOffset = positionOffset; - } - } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index b648dd5e47..49d7f67b7f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Layout; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Scoring; using osuTK; using osuTK.Graphics; @@ -36,16 +33,11 @@ namespace osu.Game.Rulesets.Osu.Statistics private GridContainer pointGrid; - private readonly BeatmapInfo beatmap; - private readonly IReadOnlyList hitEvents; - private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); + private readonly ScoreInfo score; - public Heatmap(BeatmapInfo beatmap, IReadOnlyList hitEvents) + public Heatmap(ScoreInfo score) { - this.beatmap = beatmap; - this.hitEvents = hitEvents; - - AddLayout(sizeLayout); + this.score = score; } [BackgroundDependencyLoader] @@ -149,18 +141,18 @@ namespace osu.Game.Rulesets.Osu.Statistics pointGrid.Content = points; - if (hitEvents.Count > 0) + if (score.HitEvents == null || score.HitEvents.Count == 0) + return; + + // Todo: This should probably not be done like this. + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + + foreach (var e in score.HitEvents) { - // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + if (e.LastHitObject == null || e.PositionOffset == null) + continue; - foreach (var e in hitEvents) - { - if (e.LastHitObject == null || e.PositionOffset == null) - continue; - - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); - } + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); } } diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 30d25f581f..f3ccb0630e 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Statistics { @@ -37,23 +36,23 @@ namespace osu.Game.Rulesets.Osu.Statistics /// private const float axis_points = 5; - private readonly List hitEvents; + private readonly ScoreInfo score; - public TimingDistributionGraph(List hitEvents) + public TimingDistributionGraph(ScoreInfo score) { - this.hitEvents = hitEvents; + this.score = score; } [BackgroundDependencyLoader] private void load() { - if (hitEvents.Count == 0) + if (score.HitEvents == null || score.HitEvents.Count == 0) return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = score.HitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - foreach (var e in hitEvents) + foreach (var e in score.HitEvents) { int binOffset = (int)(e.TimeOffset / binSize); bins[timing_distribution_centre_bin_index + binOffset]++; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 52cc41fbd8..d8b0594803 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,10 +9,9 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; @@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(100, 300), }, - heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) + heatmap = new TestHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -93,8 +91,8 @@ namespace osu.Game.Tests.Visual.Ranking private class TestHeatmap : Heatmap { - public TestHeatmap(BeatmapInfo beatmap, List events) - : base(beatmap, events) + public TestHeatmap(ScoreInfo score) + : base(score) { } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index cc3415a530..bcf8a19c61 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,7 +1,6 @@ // 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.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents().Cast().ToList(), + HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents() }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 178d6d95b5..d5ee50e636 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -7,9 +7,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(CreateDistributedHitEvents()) + new TimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs new file mode 100644 index 0000000000..908ac0c171 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Scoring +{ + public readonly struct HitEvent + { + /// + /// The time offset from the end of at which the event occurred. + /// + public readonly double TimeOffset; + + /// + /// The hit result. + /// + public readonly HitResult Result; + + /// + /// The on which the result occurred. + /// + public readonly HitObject HitObject; + + /// + /// The occurring prior to . + /// + [CanBeNull] + public readonly HitObject LastHitObject; + + /// + /// The player's position offset, if available, at the time of the event. + /// + [CanBeNull] + public readonly Vector2? PositionOffset; + + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) + { + TimeOffset = timeOffset; + Result = result; + HitObject = hitObject; + LastHitObject = lastHitObject; + PositionOffset = positionOffset; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 619547aef4..b9f51dfad3 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring @@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Scoring private double baseScore; private double bonusScore; + private readonly List hitEvents = new List(); + private HitObject lastHitObject; + private double scoreMultiplier = 1; public ScoreProcessor() @@ -128,6 +132,9 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore += result.Judgement.MaxNumericResult; } + hitEvents.Add(CreateHitEvent(result)); + lastHitObject = result.HitObject; + updateScore(); OnResultApplied(result); @@ -137,6 +144,9 @@ namespace osu.Game.Rulesets.Scoring { } + protected virtual HitEvent CreateHitEvent(JudgementResult result) + => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); + protected sealed override void RevertResultInternal(JudgementResult result) { Combo.Value = result.ComboAtJudgement; @@ -159,6 +169,10 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } + Debug.Assert(hitEvents.Count > 0); + lastHitObject = hitEvents[^1].LastHitObject; + hitEvents.RemoveAt(hitEvents.Count - 1); + updateScore(); OnResultReverted(result); @@ -219,6 +233,8 @@ namespace osu.Game.Rulesets.Scoring base.Reset(storeResults); scoreResultCounts.Clear(); + hitEvents.Clear(); + lastHitObject = null; if (storeResults) { @@ -259,6 +275,8 @@ namespace osu.Game.Rulesets.Scoring foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) score.Statistics[result] = GetStatistic(result); + + score.HitEvents = new List(hitEvents); } /// diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6fc5892b3c..84c0d5b54e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -168,7 +168,7 @@ namespace osu.Game.Scoring [NotMapped] [JsonIgnore] - public List HitEvents = new List(); + public List HitEvents { get; set; } [JsonIgnore] public List Files { get; set; } From 1cbbd6b4427130159832eade1e91ae557c4181e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:03:18 +0900 Subject: [PATCH 039/100] Move timing distribution graph to osu.Game --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 2 +- .../Visual/Ranking/TestSceneTimingDistributionGraph.cs | 8 ++++---- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs => osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs (96%) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 45980cb3d5..d99fee3b15 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph(score) + Child = new HitEventTimingDistributionGraph(score) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index bcf8a19c61..210abaef4e 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents() + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents() }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index d5ee50e636..bfdc216aa1 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -7,16 +7,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; using osuTK; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneTimingDistributionGraph : OsuTestScene + public class TestSceneHitEventTimingDistributionGraph : OsuTestScene { - public TestSceneTimingDistributionGraph() + public TestSceneHitEventTimingDistributionGraph() { Children = new Drawable[] { @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) + new HitEventTimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs similarity index 96% rename from osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs rename to osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index f3ccb0630e..b258e92aeb 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -12,9 +12,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Scoring; -namespace osu.Game.Rulesets.Osu.Statistics +namespace osu.Game.Screens.Ranking.Statistics { - public class TimingDistributionGraph : CompositeDrawable + public class HitEventTimingDistributionGraph : CompositeDrawable { /// /// The number of bins on each side of the timing distribution. @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; - public TimingDistributionGraph(ScoreInfo score) + public HitEventTimingDistributionGraph(ScoreInfo score) { this.score = score; } From 83e6c3efdb32c23f2af26fb2c4661ae49f62275c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:31:52 +0900 Subject: [PATCH 040/100] Adjust API for returning statistics --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 43 ++++++++----------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 3 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 18 ++++++++ .../TestSceneTimingDistributionGraph.cs | 3 +- osu.Game/Rulesets/Ruleset.cs | 1 + .../HitEventTimingDistributionGraph.cs | 26 +++++++---- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticItem.cs | 23 ++++++++++ .../Ranking/Statistics/StatisticRow.cs | 11 ++--- .../Ranking/Statistics/StatisticsPanel.cs | 5 ++- 10 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticItem.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index d99fee3b15..aa313c92b3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,7 +29,9 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using System.Linq; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -190,36 +192,29 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatistics(ScoreInfo score) { - new StatisticRow + var hitCircleEvents = score.HitEvents.Where(e => e.HitObject is HitCircle).ToList(); + + return new[] { - Content = new Drawable[] + new StatisticRow { - new StatisticContainer("Timing Distribution") + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 130, - Child = new HitEventTimingDistributionGraph(score) + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(hitCircleEvents) { - RelativeSizeAxes = Axes.Both - } - }, - new StatisticContainer("Accuracy Heatmap") - { - RelativeSizeAxes = Axes.Both, - Child = new Heatmap(score) + RelativeSizeAxes = Axes.X, + Height = 130 + }), + new StatisticItem("Accuracy Heatmap", new Heatmap(score) { - RelativeSizeAxes = Axes.Both - } - }, - }, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 130), + RelativeSizeAxes = Axes.X, + Height = 130 + }, new Dimension(GridSizeMode.Absolute, 130)), + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 49d7f67b7f..86cb8e682f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -147,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Todo: This should probably not be done like this. float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; - foreach (var e in score.HitEvents) + foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { if (e.LastHitObject == null || e.PositionOffset == null) continue; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 4cdd1fbc24..cd4e699262 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -21,9 +21,12 @@ using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; +using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko @@ -155,5 +158,20 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); + + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }), + } + } + }; } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index bfdc216aa1..b34529cca7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new HitEventTimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) + new HitEventTimingDistributionGraph(CreateDistributedHitEvents()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f05685b6e9..52784e354f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,7 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + [NotNull] public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index b258e92aeb..4acbc7da3c 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -10,10 +11,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// A graph which displays the distribution of hit timing in a series of s. + /// public class HitEventTimingDistributionGraph : CompositeDrawable { /// @@ -32,27 +36,31 @@ namespace osu.Game.Screens.Ranking.Statistics private const int timing_distribution_centre_bin_index = timing_distribution_bins; /// - /// The number of data points shown on the axis below the graph. + /// The number of data points shown on each side of the axis below the graph. /// private const float axis_points = 5; - private readonly ScoreInfo score; + private readonly IReadOnlyList hitEvents; - public HitEventTimingDistributionGraph(ScoreInfo score) + /// + /// Creates a new . + /// + /// The s to display the timing distribution of. + public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.score = score; + this.hitEvents = hitEvents; } [BackgroundDependencyLoader] private void load() { - if (score.HitEvents == null || score.HitEvents.Count == 0) + if (hitEvents == null || hitEvents.Count == 0) return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = score.HitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - foreach (var e in score.HitEvents) + foreach (var e in hitEvents) { int binOffset = (int)(e.TimeOffset / binSize); bins[timing_distribution_centre_bin_index + binOffset]++; @@ -67,6 +75,8 @@ namespace osu.Game.Screens.Ranking.Statistics InternalChild = new GridContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Width = 0.8f, Content = new[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index d7b42c1c2f..b8dde8f85e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - public class StatisticContainer : Container + internal class StatisticContainer : Container { protected override Container Content => content; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs new file mode 100644 index 0000000000..2605ae9f1b --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticItem + { + public readonly string Name; + public readonly Drawable Content; + public readonly Dimension Dimension; + + public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + { + Name = name; + Content = content; + Dimension = dimension; + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index 5d39ef57b2..ebab148fc2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -1,15 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using JetBrains.Annotations; namespace osu.Game.Screens.Ranking.Statistics { public class StatisticRow { - public Drawable[] Content = Array.Empty(); - public Dimension[] ColumnDimensions = Array.Empty(); + /// + /// The columns of this . + /// + [ItemCanBeNull] + public StatisticItem[] Columns; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index acaf91246d..3d81229ac3 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,6 +1,7 @@ // 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 System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -80,8 +81,8 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] { row.Content }, - ColumnDimensions = row.ColumnDimensions, + Content = new[] { row.Columns?.Select(c => c?.Content).ToArray() }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0).Select(i => row.Columns[i]?.Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From ad3bc99e7c9bf6d39acb5f7595d97e1369ad4c56 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:48:53 +0900 Subject: [PATCH 041/100] Fix hit event position offset not being set --- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 3 +++ osu.Game/Rulesets/Scoring/HitEvent.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 231a24cac5..86ec76e373 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -11,6 +11,9 @@ namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { + protected override HitEvent CreateHitEvent(JudgementResult result) + => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) { switch (hitObject) diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index 908ac0c171..a2770ec580 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -44,5 +44,7 @@ namespace osu.Game.Rulesets.Scoring LastHitObject = lastHitObject; PositionOffset = positionOffset; } + + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); } } From 34a8fcfd2f6a3cd1d380400801c95e02930016c2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:53:24 +0900 Subject: [PATCH 042/100] Fix potential off-by-one --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 4acbc7da3c..43de862007 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); foreach (var e in hitEvents) { From 5ce2c712d343e46e939f62d36f9ad39e047e414d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:53:43 +0900 Subject: [PATCH 043/100] Fix statistics not being wrapped by containers --- osu.Game/Rulesets/Ruleset.cs | 5 +++++ .../Ranking/Statistics/StatisticContainer.cs | 10 ++++++++-- .../Ranking/Statistics/StatisticItem.cs | 20 +++++++++++++++++++ .../Ranking/Statistics/StatisticRow.cs | 5 ++++- .../Ranking/Statistics/StatisticsPanel.cs | 11 ++++++++-- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 52784e354f..a325e641a4 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,11 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + /// + /// Creates the statistics for a to be displayed in the results screen. + /// + /// The to create the statistics for. The score is guaranteed to have populated. + /// The s to display. Each may contain 0 or more . [NotNull] public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index b8dde8f85e..b063893633 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -19,9 +19,13 @@ namespace osu.Game.Screens.Ranking.Statistics public StatisticContainer(string name) { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new GridContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Content = new[] { new Drawable[] @@ -56,13 +60,15 @@ namespace osu.Game.Screens.Ranking.Statistics { content = new Container { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, } }, }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } }; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 2605ae9f1b..a3ef5bf99e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -7,12 +7,32 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// An item to be displayed in a row of statistics inside the results screen. + /// public class StatisticItem { + /// + /// The name of this item. + /// public readonly string Name; + + /// + /// The content to be displayed. + /// public readonly Drawable Content; + + /// + /// The of this row. This can be thought of as the column dimension of an encompassing . + /// public readonly Dimension Dimension; + /// + /// Creates a new , to be displayed inside a in the results screen. + /// + /// The name of this item. + /// The content to be displayed. + /// The of this row. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) { Name = name; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index ebab148fc2..e1ca9799a3 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -5,12 +5,15 @@ using JetBrains.Annotations; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// A row of statistics to be displayed in the results screen. + /// public class StatisticRow { /// /// The columns of this . /// - [ItemCanBeNull] + [ItemNotNull] public StatisticItem[] Columns; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 3d81229ac3..328b6933a0 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -81,8 +81,15 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] { row.Columns?.Select(c => c?.Content).ToArray() }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0).Select(i => row.Columns[i]?.Dimension ?? new Dimension()).ToArray(), + Content = new[] + { + row.Columns?.Select(c => new StatisticContainer(c.Name) + { + Child = c.Content + }).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) + .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From 8aea8267fb989172c535a331aacddcfbe048e3b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:58:05 +0900 Subject: [PATCH 044/100] Add some padding --- osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index b063893633..d9e5e1294a 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 15 } } }, }, From 89a863a3379262c600f9774728e63117c8037567 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:02:20 +0900 Subject: [PATCH 045/100] Refactor OsuRuleset --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index aa313c92b3..3fb8f574b3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,29 +192,24 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] { - var hitCircleEvents = score.HitEvents.Where(e => e.HitObject is HitCircle).ToList(); - - return new[] + new StatisticRow { - new StatisticRow + Columns = new[] { - Columns = new[] + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle).ToList()) { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(hitCircleEvents) - { - RelativeSizeAxes = Axes.X, - Height = 130 - }), - new StatisticItem("Accuracy Heatmap", new Heatmap(score) - { - RelativeSizeAxes = Axes.X, - Height = 130 - }, new Dimension(GridSizeMode.Absolute, 130)), - } + RelativeSizeAxes = Axes.X, + Height = 130 + }), + new StatisticItem("Accuracy Heatmap", new Heatmap(score) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }, new Dimension(GridSizeMode.Absolute, 130)), } - }; - } + } + }; } } From 49997c54d01be791bcc1bdfe4d2ee52abf063edb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:14:17 +0900 Subject: [PATCH 046/100] Remove unused class --- osu.Game.Rulesets.Osu/Scoring/HitOffset.cs | 23 ---------------------- 1 file changed, 23 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Scoring/HitOffset.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs deleted file mode 100644 index e6a5a01b48..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; - -namespace osu.Game.Rulesets.Osu.Scoring -{ - public class HitOffset - { - public readonly Vector2 Position1; - public readonly Vector2 Position2; - public readonly Vector2 HitPosition; - public readonly float Radius; - - public HitOffset(Vector2 position1, Vector2 position2, Vector2 hitPosition, float radius) - { - Position1 = position1; - Position2 = position2; - HitPosition = hitPosition; - Radius = radius; - } - } -} From 863666f7c483d83dda88a3acf9b1bb1b33f70bce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:14:31 +0900 Subject: [PATCH 047/100] Move accuracy heatmap to osu! ruleset, rename, remove magic number --- .../TestSceneAccuracyHeatmap.cs | 16 ++++++++-------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../{Heatmap.cs => AccuracyHeatmap.cs} | 11 ++++++----- 3 files changed, 15 insertions(+), 14 deletions(-) rename {osu.Game.Tests/Visual/Ranking => osu.Game.Rulesets.Osu.Tests}/TestSceneAccuracyHeatmap.cs (86%) rename osu.Game.Rulesets.Osu/Statistics/{Heatmap.cs => AccuracyHeatmap.cs} (95%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs similarity index 86% rename from osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index d8b0594803..f2a36ea017 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -9,21 +9,21 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Ranking +namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene { private Box background; private Drawable object1; private Drawable object2; - private TestHeatmap heatmap; + private TestAccuracyHeatmap accuracyHeatmap; private ScheduledDelegate automaticAdditionDelegate; [SetUp] @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(100, 300), }, - heatmap = new TestHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) + accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Ranking RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). - heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); }, 1, true); }); @@ -85,13 +85,13 @@ namespace osu.Game.Tests.Visual.Ranking protected override bool OnMouseDown(MouseDownEvent e) { - heatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); + accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); return true; } - private class TestHeatmap : Heatmap + private class TestAccuracyHeatmap : AccuracyHeatmap { - public TestHeatmap(ScoreInfo score) + public TestAccuracyHeatmap(ScoreInfo score) : base(score) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 3fb8f574b3..65f26c0647 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 130 }), - new StatisticItem("Accuracy Heatmap", new Heatmap(score) + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score) { RelativeSizeAxes = Axes.X, Height = 130 diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs similarity index 95% rename from osu.Game.Rulesets.Osu/Statistics/Heatmap.cs rename to osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 86cb8e682f..10ca3eb9be 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; using osuTK; @@ -16,17 +17,17 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Statistics { - public class Heatmap : CompositeDrawable + public class AccuracyHeatmap : CompositeDrawable { /// - /// Size of the inner circle containing the "hit" points, relative to the size of this . + /// Size of the inner circle containing the "hit" points, relative to the size of this . /// All other points outside of the inner circle are "miss" points. /// private const float inner_portion = 0.8f; /// /// Number of rows/columns of points. - /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. /// private const int points_per_dimension = 32; @@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; - public Heatmap(ScoreInfo score) + public AccuracyHeatmap(ScoreInfo score) { this.score = score; } @@ -170,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Convert the above into the local search space. Vector2 localCentre = new Vector2(points_per_dimension) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. - double localAngle = finalAngle + 3 * Math.PI / 4; // The angle inside the heatmap on which the closest point lies. + double localAngle = finalAngle + Math.PI - MathUtils.DegreesToRadians(rotation); // The angle inside the heatmap on which the closest point lies. Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. From 81ad257a17ae2bab2df18ab9ceddb1e77202c149 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:18:58 +0900 Subject: [PATCH 048/100] Add timing distribution to mania ruleset --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index a37aaa8cc4..b8725af856 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Mania { @@ -307,6 +308,21 @@ namespace osu.Game.Rulesets.Mania { return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } + + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }), + } + } + }; } public enum PlayfieldType From 25abdc290331e6d2ffea212259771d25cbf5b3b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:41:48 +0900 Subject: [PATCH 049/100] General cleanups --- osu.Game/Rulesets/Scoring/HitEvent.cs | 18 +++++++- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 ------ osu.Game/Screens/Ranking/ScorePanel.cs | 18 +++++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 43 ++++++++++++++----- .../Ranking/ScorePanelTrackingContainer.cs | 17 +++++++- .../Ranking/Statistics/StatisticContainer.cs | 23 ++++++---- .../Ranking/Statistics/StatisticItem.cs | 4 +- .../Ranking/Statistics/StatisticsPanel.cs | 9 ++-- 9 files changed, 98 insertions(+), 48 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index a2770ec580..ea2975a6c4 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -7,6 +7,9 @@ using osuTK; namespace osu.Game.Rulesets.Scoring { + /// + /// A generated by the containing extra statistics around a . + /// public readonly struct HitEvent { /// @@ -31,11 +34,19 @@ namespace osu.Game.Rulesets.Scoring public readonly HitObject LastHitObject; /// - /// The player's position offset, if available, at the time of the event. + /// A position offset, if available, at the time of the event. /// [CanBeNull] public readonly Vector2? PositionOffset; + /// + /// Creates a new . + /// + /// The time offset from the end of at which the event occurs. + /// The . + /// The that triggered the event. + /// The previous . + /// A positional offset. public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) { TimeOffset = timeOffset; @@ -45,6 +56,11 @@ namespace osu.Game.Rulesets.Scoring PositionOffset = positionOffset; } + /// + /// Creates a new with an optional positional offset. + /// + /// The positional offset. + /// The new . public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 77acbd4137..018b50bd3d 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Scoring /// Retrieves the with the largest hit window that produces a successful hit. /// /// The lowest allowed successful . - public HitResult LowestSuccessfulHitResult() + protected HitResult LowestSuccessfulHitResult() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b9f51dfad3..22ec023f58 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -136,12 +136,6 @@ namespace osu.Game.Rulesets.Scoring lastHitObject = result.HitObject; updateScore(); - - OnResultApplied(result); - } - - protected virtual void OnResultApplied(JudgementResult result) - { } protected virtual HitEvent CreateHitEvent(JudgementResult result) @@ -174,12 +168,6 @@ namespace osu.Game.Rulesets.Scoring hitEvents.RemoveAt(hitEvents.Count - 1); updateScore(); - - OnResultReverted(result); - } - - protected virtual void OnResultReverted(JudgementResult result) - { } private void updateScore() diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 257279bdc9..9633f5c533 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -76,12 +76,11 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; - public Action PostExpandAction; /// - /// Whether this can enter into an state. + /// An action to be invoked if this is clicked while in an expanded state. /// - public bool CanExpand = true; + public Action PostExpandAction; public readonly ScoreInfo Score; @@ -250,6 +249,7 @@ namespace osu.Game.Screens.Ranking { base.Size = value; + // Auto-size isn't used to avoid 1-frame issues and because the score panel is removed/re-added to the container. if (trackingContainer != null) trackingContainer.Size = value; } @@ -259,8 +259,7 @@ namespace osu.Game.Screens.Ranking { if (State == PanelState.Contracted) { - if (CanExpand) - State = PanelState.Expanded; + State = PanelState.Expanded; return true; } @@ -276,6 +275,15 @@ namespace osu.Game.Screens.Ranking private ScorePanelTrackingContainer trackingContainer; + /// + /// Creates a which this can reside inside. + /// The will track the size of this . + /// + /// + /// This is immediately added as a child of the . + /// + /// The . + /// If a already exists. public ScorePanelTrackingContainer CreateTrackingContainer() { if (trackingContainer != null) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 8f9064c2d1..9ebd7822c0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,12 +26,13 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + /// + /// An action to be invoked if a is clicked while in an expanded state. + /// public Action PostExpandAction; public readonly Bindable SelectedScore = new Bindable(); - public float CurrentScrollPosition => scroll.Current; - private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -47,16 +48,13 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. - Children = new Drawable[] + Child = flow = new Flow { - flow = new Flow - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(panel_spacing, 0), - AutoSizeAxes = Axes.Both, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, } }; } @@ -166,6 +164,10 @@ namespace osu.Game.Screens.Ranking private bool handleInput = true; + /// + /// Whether this or any of the s contained should handle scroll or click input. + /// Setting to false will also hide the scrollbar. + /// public bool HandleInput { get => handleInput; @@ -180,10 +182,24 @@ namespace osu.Game.Screens.Ranking public override bool PropagateNonPositionalInputSubTree => HandleInput && base.PropagateNonPositionalInputSubTree; + /// + /// Enumerates all s contained in this . + /// + /// public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); + /// + /// Finds the corresponding to a . + /// + /// The to find the corresponding for. + /// The . public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel; + /// + /// Detaches a from its , allowing the panel to be moved elsewhere in the hierarchy. + /// + /// The to detach. + /// If is not a part of this . public void Detach(ScorePanel panel) { var container = flow.FirstOrDefault(t => t.Panel == panel); @@ -193,6 +209,11 @@ namespace osu.Game.Screens.Ranking container.Detach(); } + /// + /// Attaches a to its in this . + /// + /// The to attach. + /// If is not a part of this . public void Attach(ScorePanel panel) { var container = flow.FirstOrDefault(t => t.Panel == panel); diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index f6f26d0f8a..c8010d1c32 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -6,16 +6,27 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking { + /// + /// A which tracks the size of a , to which the can be added or removed. + /// public class ScorePanelTrackingContainer : CompositeDrawable { + /// + /// The that created this . + /// public readonly ScorePanel Panel; - public ScorePanelTrackingContainer(ScorePanel panel) + internal ScorePanelTrackingContainer(ScorePanel panel) { Panel = panel; Attach(); } + /// + /// Detaches the from this , removing it as a child. + /// This will continue tracking any size changes. + /// + /// If the is already detached. public void Detach() { if (InternalChildren.Count == 0) @@ -24,6 +35,10 @@ namespace osu.Game.Screens.Ranking RemoveInternal(Panel); } + /// + /// Attaches the to this , adding it as a child. + /// + /// If the is already attached. public void Attach() { if (InternalChildren.Count > 0) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index d9e5e1294a..ed98698411 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics.CodeAnalysis; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,13 +12,16 @@ using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - internal class StatisticContainer : Container + /// + /// Wraps a to add a header and suitable layout for use in . + /// + internal class StatisticContainer : CompositeDrawable { - protected override Container Content => content; - - private readonly Container content; - - public StatisticContainer(string name) + /// + /// Creates a new . + /// + /// The to display. + public StatisticContainer([NotNull] StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -50,7 +54,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = name, + Text = item.Name, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), } } @@ -58,11 +62,12 @@ namespace osu.Game.Screens.Ranking.Statistics }, new Drawable[] { - content = new Container + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 } + Margin = new MarginPadding { Top = 15 }, + Child = item.Content } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index a3ef5bf99e..e959ed24fc 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,9 +30,9 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of this item. + /// The name of the item. /// The content to be displayed. - /// The of this row. This can be thought of as the column dimension of an encompassing . + /// The of this item. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) { Name = name; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 328b6933a0..c560cc9852 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -83,10 +83,7 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c.Name) - { - Child = c.Content - }).Cast().ToArray() + row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() }, ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), @@ -105,8 +102,8 @@ namespace osu.Game.Screens.Ranking.Statistics } } - protected override void PopIn() => this.FadeIn(); + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(); + protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); } } From 49bdd897758bf918224e8fafa8a0e02e9d0af70a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:54:09 +0900 Subject: [PATCH 050/100] Cleanup ReplayPlayer adjustments --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83991ad027..cfcef5155d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -460,7 +460,7 @@ namespace osu.Game.Screens.Play { var score = new ScoreInfo { - Beatmap = Beatmap.Value.BeatmapInfo, + Beatmap = gameplayBeatmap.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index d7580ea271..8a925958fd 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Screens; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -25,16 +24,18 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(score); } - protected override void GotoRanking() - { - this.Push(CreateResults(CreateScore())); - } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - // protected override ScoreInfo CreateScore() - // { - // return score.ScoreInfo; - // } + protected override ScoreInfo CreateScore() + { + var baseScore = base.CreateScore(); + + // Since the replay score doesn't contain statistics, we'll pass them through here. + // We also have to pass in the beatmap to get the post-mod-application version. + score.ScoreInfo.Beatmap = baseScore.Beatmap; + score.ScoreInfo.HitEvents = baseScore.HitEvents; + + return score.ScoreInfo; + } } } From 740b01c049592483adb0dbc6f8e0b2d4cbf9e9d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:05:58 +0900 Subject: [PATCH 051/100] Add xmldoc --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 22ec023f58..9c1bc35169 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -138,6 +138,11 @@ namespace osu.Game.Rulesets.Scoring updateScore(); } + /// + /// Creates the that describes a . + /// + /// The to describe. + /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); From 486b899e8f31c262b5ab911792bb5269c0edc880 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:11:29 +0900 Subject: [PATCH 052/100] Rename method --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b8725af856..44e8f343d5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -309,7 +309,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 65f26c0647..8222eba339 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index cd4e699262..92b04e8397 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index a325e641a4..f9c2b09be9 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -216,6 +216,6 @@ namespace osu.Game.Rulesets /// The to create the statistics for. The score is guaranteed to have populated. /// The s to display. Each may contain 0 or more . [NotNull] - public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); + public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c560cc9852..efb9397a23 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics Spacing = new Vector2(30, 15), }; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatistics(newScore)) + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore)) { rows.Add(new GridContainer { From 4cb49cd606a57ec3aa4770ad8ab1caf5b11eeaee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:21:34 +0900 Subject: [PATCH 053/100] Add minimum height to the timing distribution graph --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 43de862007..9b46bea2cb 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Ranking.Statistics int maxCount = bins.Max(); var bars = new Drawable[total_timing_distribution_bins]; for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)bins[i] / maxCount }; + bars[i] = new Bar { Height = Math.Max(0.05f, (float)bins[i] / maxCount) }; Container axisFlow; From 2814433d7cf572a78218db8da6c055e7139e1962 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:22:07 +0900 Subject: [PATCH 054/100] Rename test file --- ...butionGraph.cs => TestSceneHitEventTimingDistributionGraph.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Ranking/{TestSceneTimingDistributionGraph.cs => TestSceneHitEventTimingDistributionGraph.cs} (100%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs similarity index 100% rename from osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs From 037bd3b46330bc2f2942bf761f0e883455b02d5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:47:55 +0900 Subject: [PATCH 055/100] Fix possible nullref --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 6 ++++++ osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 210abaef4e..8700fbeb42 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -29,6 +29,12 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); } + [Test] + public void TestNullScore() + { + loadPanel(null); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index efb9397a23..cac2bf866b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -62,6 +62,9 @@ namespace osu.Game.Screens.Ranking.Statistics var newScore = score.NewValue; + if (newScore == null) + return; + if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) content.Add(new MessagePlaceholder("Score has no statistics :(")); else From 3ede095b9c88e3d2f99a483c8b6171eb7f889f03 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 15:42:55 +0900 Subject: [PATCH 056/100] Apply refactorings from review --- osu.Game/Screens/Ranking/ResultsScreen.cs | 20 ++++++++------------ osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 133efd6e7b..193d975e42 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -236,13 +236,11 @@ namespace osu.Game.Screens.Ranking scorePanelList.Detach(expandedPanel); detachedPanelContainer.Add(expandedPanel); - // Move into its original location in the local container. + // Move into its original location in the local container first, then to the final location. var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); - expandedPanel.MoveTo(origLocation); - expandedPanel.MoveToX(origLocation.X); - - // Move into the final location. - expandedPanel.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + expandedPanel.MoveTo(origLocation) + .Then() + .MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint); // Hide contracted panels. foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -262,13 +260,11 @@ namespace osu.Game.Screens.Ranking detachedPanelContainer.Remove(detachedPanel); scorePanelList.Attach(detachedPanel); - // Move into its original location in the attached container. + // Move into its original location in the attached container first, then to the final location. var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos); - detachedPanel.MoveTo(origLocation); - detachedPanel.MoveToX(origLocation.X); - - // Move into the final location. - detachedPanel.MoveToX(0, 150, Easing.OutQuint); + detachedPanel.MoveTo(origLocation) + .Then() + .MoveTo(new Vector2(0, origLocation.Y), 150, Easing.OutQuint); // Show contracted panels. foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 9ebd7822c0..0f8bc82ac0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Ranking /// If is not a part of this . public void Detach(ScorePanel panel) { - var container = flow.FirstOrDefault(t => t.Panel == panel); + var container = flow.SingleOrDefault(t => t.Panel == panel); if (container == null) throw new InvalidOperationException("Panel is not contained by the score panel list."); @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Ranking /// If is not a part of this . public void Attach(ScorePanel panel) { - var container = flow.FirstOrDefault(t => t.Panel == panel); + var container = flow.SingleOrDefault(t => t.Panel == panel); if (container == null) throw new InvalidOperationException("Panel is not contained by the score panel list."); From 21f776e51feafd8a06e397e90ef88bace4900d0c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 15:48:42 +0900 Subject: [PATCH 057/100] Simplify/optimise heatmap point additoin --- .../Statistics/AccuracyHeatmap.cs | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 10ca3eb9be..f05bfce8d7 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -175,25 +174,10 @@ namespace osu.Game.Rulesets.Osu.Statistics Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. - double minDist = double.PositiveInfinity; - HitPoint point = null; + int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); + int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); - for (int r = 0; r < points_per_dimension; r++) - { - for (int c = 0; c < points_per_dimension; c++) - { - float dist = Vector2.Distance(new Vector2(c, r), localPoint); - - if (dist < minDist) - { - minDist = dist; - point = (HitPoint)pointGrid.Content[r][c]; - } - } - } - - Debug.Assert(point != null); - point.Increment(); + ((HitPoint)pointGrid.Content[r][c]).Increment(); } private class HitPoint : Circle From 533d6e72eb5dc403408b0917af2ca431e2890e86 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:05:21 +0900 Subject: [PATCH 058/100] Refactor + comment angle math --- .../Statistics/AccuracyHeatmap.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f05bfce8d7..f8ab03aad0 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -167,11 +167,28 @@ namespace osu.Game.Rulesets.Osu.Statistics double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - // Convert the above into the local search space. + // Consider two objects placed horizontally, with the start on the left and the end on the right. + // The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form: + // +pi | 0 + // O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi) + // -pi | 0 + // E.g. If the hit point was directly above end, it would have an angle pi/2. + // + // It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form: + // 0 | pi + // O --------- O -----> + // 2pi | pi + // + // However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted. + // Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted. + // + // We also need to apply the anti-clockwise rotation. + var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation); + var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); + Vector2 localCentre = new Vector2(points_per_dimension) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. - double localAngle = finalAngle + Math.PI - MathUtils.DegreesToRadians(rotation); // The angle inside the heatmap on which the closest point lies. - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; // Find the most relevant hit point. int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); From 9dbd230ad30f9f4e8564ae986c0f66f99415b131 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:06:52 +0900 Subject: [PATCH 059/100] Don't consider slider tails in timing distribution --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8222eba339..a164265290 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle).ToList()) + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) { RelativeSizeAxes = Axes.X, Height = 130 From 261adfc4e682973738960c6ace4c284f502811fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:38:41 +0900 Subject: [PATCH 060/100] Create a local playable beatmap instead --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../TestSceneAccuracyHeatmap.cs | 6 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 +- .../Statistics/AccuracyHeatmap.cs | 7 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 3 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 2 - .../Ranking/Statistics/StatisticsPanel.cs | 67 ++++++++++++------- 9 files changed, 57 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f8fa5d4c40..411956e120 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index f2a36ea017..49b469ba24 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333"), }, - object1 = new BorderCircle + object2 = new BorderCircle { Position = new Vector2(256, 192), Colour = Color4.Yellow, }, - object2 = new BorderCircle + object1 = new BorderCircle { Position = new Vector2(100, 300), }, @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAccuracyHeatmap : AccuracyHeatmap { public TestAccuracyHeatmap(ScoreInfo score) - : base(score) + : base(score, new TestBeatmap(new OsuRuleset().RulesetInfo)) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a164265290..2ba2f4b097 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { @@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 130 }), - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score) + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, Height = 130 diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f8ab03aad0..58089553a4 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; using osuTK; @@ -35,10 +36,12 @@ namespace osu.Game.Rulesets.Osu.Statistics private GridContainer pointGrid; private readonly ScoreInfo score; + private readonly IBeatmap playableBeatmap; - public AccuracyHeatmap(ScoreInfo score) + public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; + this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -146,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Statistics return; // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 92b04e8397..17d0800228 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f9c2b09be9..3a7f433a37 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -214,8 +214,9 @@ namespace osu.Game.Rulesets /// Creates the statistics for a to be displayed in the results screen. /// /// The to create the statistics for. The score is guaranteed to have populated. + /// The , converted for this with all relevant s applied. /// The s to display. Each may contain 0 or more . [NotNull] - public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => Array.Empty(); + public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c2bb75b8f3..d3b88e56ae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -463,7 +463,7 @@ namespace osu.Game.Screens.Play { var score = new ScoreInfo { - Beatmap = gameplayBeatmap.BeatmapInfo, + Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8a925958fd..7f5c17a265 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - // We also have to pass in the beatmap to get the post-mod-application version. - score.ScoreInfo.Beatmap = baseScore.Beatmap; score.ScoreInfo.HitEvents = baseScore.HitEvents; return score.ScoreInfo; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cac2bf866b..8aceaa335c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,14 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osuTK; @@ -22,6 +26,9 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool StartHidden => true; + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private readonly Container content; private readonly LoadingSpinner spinner; @@ -71,37 +78,47 @@ namespace osu.Game.Screens.Ranking.Statistics { spinner.Show(); - var rows = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - }; + var localCancellationSource = loadCancellation = new CancellationTokenSource(); + IBeatmap playableBeatmap = null; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore)) + // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. + Task.Run(() => { - rows.Add(new GridContainer + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + }, loadCancellation.Token).ContinueWith(t => + { + var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 15), + }; + + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + { + rows.Add(new GridContainer { - row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) - .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) + .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } - LoadComponentAsync(rows, d => - { - if (Score.Value != newScore) - return; + LoadComponentAsync(rows, d => + { + if (Score.Value != newScore) + return; - spinner.Hide(); - content.Add(d); - }, (loadCancellation = new CancellationTokenSource()).Token); + spinner.Hide(); + content.Add(d); + }, localCancellationSource.Token); + }, localCancellationSource.Token); } } From 2b7fb2b71d352a97031b7477315c50f0a9f8ba70 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:04:51 +0900 Subject: [PATCH 061/100] Rename to Position --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- osu.Game/Rulesets/Scoring/HitEvent.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 58089553a4..40bdeeaa88 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -153,10 +153,10 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { - if (e.LastHitObject == null || e.PositionOffset == null) + if (e.LastHitObject == null || e.Position == null) continue; - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius); } } diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index ea2975a6c4..0ebbec62ba 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Scoring public readonly HitObject LastHitObject; /// - /// A position offset, if available, at the time of the event. + /// A position, if available, at the time of the event. /// [CanBeNull] - public readonly Vector2? PositionOffset; + public readonly Vector2? Position; /// /// Creates a new . @@ -46,14 +46,14 @@ namespace osu.Game.Rulesets.Scoring /// The . /// The that triggered the event. /// The previous . - /// A positional offset. - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) + /// A position corresponding to the event. + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; - PositionOffset = positionOffset; + Position = position; } /// From 30aa6ec2d3ff6dc63c0bf9b3d33cb5aef820c35c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:05:41 +0900 Subject: [PATCH 062/100] Don't consider slider tails in accuracy heatmap --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 40bdeeaa88..cba753e003 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Todo: This should probably not be done like this. float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; - foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) + foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) { if (e.LastHitObject == null || e.Position == null) continue; From 988baad16f296bb4f3df9f2c5e3478651057ceff Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:20:43 +0900 Subject: [PATCH 063/100] Expand statistics to fill more of the screen --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 13 +++++++++---- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../Screens/Ranking/Statistics/StatisticsPanel.cs | 8 +++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 411956e120..a27485dd06 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -320,7 +320,7 @@ namespace osu.Game.Rulesets.Mania new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2ba2f4b097..e488ba65c8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -30,7 +30,6 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -201,13 +200,19 @@ namespace osu.Game.Rulesets.Osu new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), + } + }, + new StatisticRow + { + Columns = new[] + { new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 130 - }, new Dimension(GridSizeMode.Absolute, 130)), + Height = 250 + }), } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 17d0800228..156905fa9c 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8aceaa335c..d2d2adb2f4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -98,11 +98,17 @@ namespace osu.Game.Screens.Ranking.Statistics { rows.Add(new GridContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() + row.Columns?.Select(c => new StatisticContainer(c) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }).Cast().ToArray() }, ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), From 5c4df2e32c0a1cd8f79f8d5e1e99e9fef6bfd228 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:20:42 +0900 Subject: [PATCH 064/100] Cancel load on dispose --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index d2d2adb2f4..651cdc4b0f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.Ranking.Statistics private void populateStatistics(ValueChangedEvent score) { loadCancellation?.Cancel(); + loadCancellation = null; foreach (var child in content) child.FadeOut(150).Expire(); @@ -131,5 +132,12 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); + + protected override void Dispose(bool isDisposing) + { + loadCancellation?.Cancel(); + + base.Dispose(isDisposing); + } } } From ff2f3a8484022a209b09d5c72343561e26e4128a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:32:04 +0900 Subject: [PATCH 065/100] Fix div-by-zero errors with autoplay --- ...estSceneHitEventTimingDistributionGraph.cs | 28 ++++++++++++++++--- .../HitEventTimingDistributionGraph.cs | 4 +++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index b34529cca7..7ca1fc842f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -15,7 +17,25 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneHitEventTimingDistributionGraph : OsuTestScene { - public TestSceneHitEventTimingDistributionGraph() + [Test] + public void TestManyDistributedEvents() + { + createTest(CreateDistributedHitEvents()); + } + + [Test] + public void TestZeroTimeOffset() + { + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + } + + [Test] + public void TestNoEvents() + { + createTest(new List()); + } + + private void createTest(List events) => AddStep("create test", () => { Children = new Drawable[] { @@ -24,14 +44,14 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new HitEventTimingDistributionGraph(CreateDistributedHitEvents()) + new HitEventTimingDistributionGraph(events) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(400, 130) + Size = new Vector2(600, 130) } }; - } + }); public static List CreateDistributedHitEvents() { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 9b46bea2cb..8ec7e863b1 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,8 +58,12 @@ namespace osu.Game.Screens.Ranking.Statistics return; int[] bins = new int[total_timing_distribution_bins]; + double binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); + // Prevent div-by-0 by enforcing a minimum bin size + binSize = Math.Max(1, binSize); + foreach (var e in hitEvents) { int binOffset = (int)(e.TimeOffset / binSize); From 6afd6efdeba5fe08b9598c9de1c089a162bcc467 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:33:08 +0900 Subject: [PATCH 066/100] Return default beatmap if local beatmap can't be retrieved --- osu.Game/Beatmaps/BeatmapManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2cf3a21975..637833fb5d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -240,6 +240,9 @@ namespace osu.Game.Beatmaps beatmapInfo = QueryBeatmap(b => b.ID == info.ID); } + if (beatmapInfo == null) + return DefaultBeatmap; + lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); From 983f0ada2da68527f5892ffd6ff0202c05d1d439 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:44:39 +0900 Subject: [PATCH 067/100] Increase number of points to ensure there's a centre --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index cba753e003..23539f3a12 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -27,9 +27,9 @@ namespace osu.Game.Rulesets.Osu.Statistics /// /// Number of rows/columns of points. - /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// ~4px per point @ 128x128 size (the contents of the are always square). 1089 total points. /// - private const int points_per_dimension = 32; + private const int points_per_dimension = 33; private const float rotation = 45; From 1aec1ea53fc9566e384f03fd96e5e5af72f3a2be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:45:44 +0900 Subject: [PATCH 068/100] Fix off-by-one causing auto to not be centred --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 23539f3a12..0d6d05292a 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Statistics var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation); var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); - Vector2 localCentre = new Vector2(points_per_dimension) / 2; + Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; From beb6e6ea88af3dbf213808accea49d247e358ce8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:00:13 +0900 Subject: [PATCH 069/100] Buffer the accuracy heatmap for performance --- .../Statistics/AccuracyHeatmap.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 0d6d05292a..6e1b6ef9b5 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; + private BufferedContainer bufferedGrid; private GridContainer pointGrid; private readonly ScoreInfo score; @@ -112,10 +113,16 @@ namespace osu.Game.Rulesets.Osu.Statistics } } }, - pointGrid = new GridContainer + bufferedGrid = new BufferedContainer { - RelativeSizeAxes = Axes.Both - } + RelativeSizeAxes = Axes.Both, + CacheDrawnFrameBuffer = true, + BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0), + Child = pointGrid = new GridContainer + { + RelativeSizeAxes = Axes.Both + } + }, } }; @@ -198,6 +205,8 @@ namespace osu.Game.Rulesets.Osu.Statistics int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); ((HitPoint)pointGrid.Content[r][c]).Increment(); + + bufferedGrid.ForceRedraw(); } private class HitPoint : Circle From b3e200ee7face7f246ef82ee56faba604a216740 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:00:35 +0900 Subject: [PATCH 070/100] Re-invert test --- osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index 49b469ba24..10d9d7ffde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333"), }, - object2 = new BorderCircle + object1 = new BorderCircle { Position = new Vector2(256, 192), Colour = Color4.Yellow, }, - object1 = new BorderCircle + object2 = new BorderCircle { Position = new Vector2(100, 300), }, From cb03e6faa9cf975719a8d3753ca063bb986ed8bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:09:47 +0900 Subject: [PATCH 071/100] Improve visual display of arrow --- .../Statistics/AccuracyHeatmap.cs | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 6e1b6ef9b5..94d47ecb32 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -74,41 +74,52 @@ namespace osu.Game.Rulesets.Osu.Statistics new Container { RelativeSizeAxes = Axes.Both, - Masking = true, Children = new Drawable[] { - new Box + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(1), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + } + }, }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 10, - Height = 2f, + Height = 2, }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Y = -1, - Width = 2f, + Width = 2, Height = 10, } } From 1bf00e0c820c8c391a0b7574b8cfc28ea0374c43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 23:22:49 +0900 Subject: [PATCH 072/100] Schedule continuation --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 651cdc4b0f..77f3bd7b5c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics Task.Run(() => { playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); - }, loadCancellation.Token).ContinueWith(t => + }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer { @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); }, localCancellationSource.Token); - }, localCancellationSource.Token); + }), localCancellationSource.Token); } } From 6d19fd936ef5e05fd0e95fc4aa12417e29d3f36c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 15:13:30 +0900 Subject: [PATCH 073/100] Change test scene to not inherit unused ScreenTestScene --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ac364b5233..f5c5a4d75c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -25,7 +25,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { [TestFixture] - public class TestSceneResultsScreen : ScreenTestScene + public class TestSceneResultsScreen : OsuManualInputManagerTestScene { private BeatmapManager beatmaps; From 6bcc693c2f8fc80649e5af944bf87c1e8c946145 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 15:21:23 +0900 Subject: [PATCH 074/100] Add ability to close statistics by clicking anywhere --- .../Visual/Ranking/TestSceneResultsScreen.cs | 40 +++++++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 7 ++++ 2 files changed, 47 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index f5c5a4d75c..74808bc2f5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Statistics; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -87,6 +88,45 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestShowHideStatisticsViaOutsideClick() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddUntilStep("expanded panel at the left of the screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150; + }); + + AddStep("click to right of panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddUntilStep("expanded panel in centre of screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1); + }); + } + [Test] public void TestShowHideStatistics() { diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 77f3bd7b5c..7f406331cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; @@ -129,6 +130,12 @@ namespace osu.Game.Screens.Ranking.Statistics } } + protected override bool OnClick(ClickEvent e) + { + ToggleVisibility(); + return true; + } + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); From 0d3bc1ac29685628e57cceddd94d525b6ef48ea2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 22:29:30 +0900 Subject: [PATCH 075/100] Add basic heatmap colour scaling based on peak value --- .../Statistics/AccuracyHeatmap.cs | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 94d47ecb32..eeb8b519ca 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; + /// + /// The highest count of any point currently being displayed. + /// + protected float PeakValue { get; private set; } + public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; @@ -152,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Statistics ? HitPointType.Hit : HitPointType.Miss; - var point = new HitPoint(pointType) + var point = new HitPoint(pointType, this) { Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) }; @@ -215,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Statistics int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); - ((HitPoint)pointGrid.Content[r][c]).Increment(); + PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); bufferedGrid.ForceRedraw(); } @@ -223,21 +229,56 @@ namespace osu.Game.Rulesets.Osu.Statistics private class HitPoint : Circle { private readonly HitPointType pointType; + private readonly AccuracyHeatmap heatmap; - public HitPoint(HitPointType pointType) + public override bool IsPresent => count > 0; + + public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap) { this.pointType = pointType; + this.heatmap = heatmap; RelativeSizeAxes = Axes.Both; - Alpha = 0; + Alpha = 1; } - public void Increment() + private int count; + + /// + /// Increment the value of this point by one. + /// + /// The value after incrementing. + public int Increment() { - if (Alpha < 1) - Alpha += 0.1f; - else if (pointType == HitPointType.Hit) - Colour = ((Color4)Colour).Lighten(0.1f); + return ++count; + } + + protected override void Update() + { + base.Update(); + + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; + + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; + + float amount = 0; + + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, count / 10f); + + // add relative portion + amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + + Debug.Assert(amount <= 1); + + Alpha = Math.Min(amount / lighten_cutoff, 1); + if (pointType == HitPointType.Hit) + Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff)); } } From fd13c0a6ddb5c8ea3260e64f956af86ab63bee5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 18:44:04 +0900 Subject: [PATCH 076/100] Standardise line thickness --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index eeb8b519ca..89707b3ebb 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; + private const float line_thickness = 2; + /// /// The highest count of any point currently being displayed. /// @@ -69,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Statistics RelativeSizeAxes = Axes.Both, Size = new Vector2(inner_portion), Masking = true, - BorderThickness = 2f, + BorderThickness = line_thickness, BorderColour = Color4.White, Child = new Box { @@ -98,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Statistics Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, + Width = line_thickness, Rotation = -rotation, Alpha = 0.3f, }, @@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Statistics Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, + Width = line_thickness, Rotation = rotation }, } From c095753f2444ce052b8fa7588c9cfb1eb0956cef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 19:02:04 +0900 Subject: [PATCH 077/100] Add better line smoothing --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 89707b3ebb..20adbc1c02 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -98,9 +98,10 @@ namespace osu.Game.Rulesets.Osu.Statistics { Anchor = Anchor.Centre, Origin = Anchor.Centre, + EdgeSmoothness = new Vector2(1), RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = line_thickness, + Width = line_thickness / 2, Rotation = -rotation, Alpha = 0.3f, }, @@ -108,9 +109,10 @@ namespace osu.Game.Rulesets.Osu.Statistics { Anchor = Anchor.Centre, Origin = Anchor.Centre, + EdgeSmoothness = new Vector2(1), RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = line_thickness, + Width = line_thickness / 2, // adjust for edgesmoothness Rotation = rotation }, } @@ -121,13 +123,15 @@ namespace osu.Game.Rulesets.Osu.Statistics Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 10, - Height = 2, + EdgeSmoothness = new Vector2(1), + Height = line_thickness / 2, // adjust for edgesmoothness }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Width = 2, + EdgeSmoothness = new Vector2(1), + Width = line_thickness / 2, // adjust for edgesmoothness Height = 10, } } From 97a212a7f6a35a17a098e9ae11ff2a9b27833666 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 14:32:01 +0200 Subject: [PATCH 078/100] Hide red tint based on "Show health display even when you can't fail" setting --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 25 ++++++++++++++++++++++- osu.Game/Screens/Play/HUDOverlay.cs | 5 +++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index a49aa89a7c..6fda5a1214 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + public readonly Bindable HUDEnabled = new Bindable(); private readonly Bindable enabled = new Bindable(); private readonly Container boxes; @@ -74,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); + enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue ? true : false), true); } protected override void LoadComplete() @@ -105,6 +106,28 @@ namespace osu.Game.Screens.Play.HUD enabled.Value = false; } + /// + /// Tries to fade based on "Fade playfield when health is low" setting + /// + /// Duration of the fade + /// Type of easing + /// True when you want to fade in, false when you want to fade out + public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) + { + if (HUDEnabled.Value) + { + if (fadeIn) + { + if (enabled.Value) + this.FadeIn(fadeDuration, easing); + } + else + this.FadeOut(fadeDuration, easing); + } + else + this.FadeOut(fadeDuration, easing); + } + protected override void Update() { double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 5114efd9a9..73b93582ef 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Diagnostics.Runtime.Interop; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -153,6 +154,8 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); + + FailingLayer.HUDEnabled.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); @@ -168,11 +171,13 @@ namespace osu.Game.Screens.Play if (healthBar.NewValue) { HealthDisplay.FadeIn(fade_duration, fade_easing); + FailingLayer.TryToFade(fade_duration, fade_easing, true); topScoreContainer.MoveToY(30, fade_duration, fade_easing); } else { HealthDisplay.FadeOut(fade_duration, fade_easing); + FailingLayer.TryToFade(fade_duration, fade_easing, false); topScoreContainer.MoveToY(0, fade_duration, fade_easing); } }, true); From efeaa1cc10ddd6d0b80ebd16651908a4be7c818a Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 14:58:42 +0200 Subject: [PATCH 079/100] Make some changes, fix and add tests --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 27 +++++++++++++++++++ osu.Game/Screens/Play/HUD/FailingLayer.cs | 3 ++- osu.Game/Screens/Play/HUDOverlay.cs | 2 -- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index a95e806862..83d9e888f1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; @@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; + private Bindable enabledHUD = new Bindable(); + [Resolved] private OsuConfigManager config { get; set; } @@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = layer = new FailingLayer(); layer.BindHealthProcessor(new DrainingHealthProcessor(1)); + layer.HUDEnabled.BindTo(enabledHUD); }); + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } + + [Test] + public void TestLayerVisibilityWithDifferentOptions() + { + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + + AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer fade is visible", () => layer.IsPresent); + } } } diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 6fda5a1214..d982764c30 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue ? true : false), true); + enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + HUDEnabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 73b93582ef..d4c548dce7 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -171,13 +171,11 @@ namespace osu.Game.Screens.Play if (healthBar.NewValue) { HealthDisplay.FadeIn(fade_duration, fade_easing); - FailingLayer.TryToFade(fade_duration, fade_easing, true); topScoreContainer.MoveToY(30, fade_duration, fade_easing); } else { HealthDisplay.FadeOut(fade_duration, fade_easing); - FailingLayer.TryToFade(fade_duration, fade_easing, false); topScoreContainer.MoveToY(0, fade_duration, fade_easing); } }, true); From 798e8e7a8deea5d1ac665bc9491604c0f082e5ed Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 15:12:01 +0200 Subject: [PATCH 080/100] Fix CI fail --- osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 83d9e888f1..3eda47627b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; - private Bindable enabledHUD = new Bindable(); + private readonly Bindable enabledHUD = new Bindable(); [Resolved] private OsuConfigManager config { get; set; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d4c548dce7..b55a93db1f 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using Microsoft.Diagnostics.Runtime.Interop; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; From bd1f38cc3ef41c0ca8bbd586c81b1cbf8b6a6d9f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 23:21:44 +0900 Subject: [PATCH 081/100] Fix crash due to unsafe mod deserialisation --- .../Online/TestAPIModSerialization.cs | 59 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs index d9318aa822..5948582d77 100644 --- a/osu.Game.Tests/Online/TestAPIModSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs @@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); } + [Test] + public void TestDeserialiseTimeRampMod() + { + // Create the mod with values different from default. + var apiMod = new APIMod(new TestModTimeRamp + { + AdjustPitch = { Value = false }, + InitialRate = { Value = 1.25 }, + FinalRate = { Value = 0.25 } + }); + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset()); + + Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false)); + Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25)); + Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); + } + private class TestRuleset : Ruleset { - public override IEnumerable GetModsFor(ModType type) => new[] { new TestMod() }; + public override IEnumerable GetModsFor(ModType type) => new Mod[] + { + new TestMod(), + new TestModTimeRamp(), + }; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); @@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online Precision = 0.01, }; } + + private class TestModTimeRamp : ModTimeRamp + { + public override string Name => "Test Mod"; + public override string Acronym => "TMTR"; + public override double ScoreMultiplier => 1; + + [SettingSource("Initial rate", "The starting speed of the track")] + public override BindableNumber InitialRate { get; } = new BindableDouble + { + MinValue = 1, + MaxValue = 2, + Default = 1.5, + Value = 1.5, + Precision = 0.01, + }; + + [SettingSource("Final rate", "The speed increase to ramp towards")] + public override BindableNumber FinalRate { get; } = new BindableDouble + { + MinValue = 0, + MaxValue = 1, + Default = 0.5, + Value = 0.5, + Precision = 0.01, + }; + + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public override BindableBool AdjustPitch { get; } = new BindableBool + { + Default = true, + Value = true + }; + } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index cbd07efa97..839d97f04e 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Mods private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { // remove existing old adjustment - track.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); } private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) From c233dc476800e7df39ee242a7515cc6bc8beb5e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 27 Jun 2020 00:16:16 +0900 Subject: [PATCH 082/100] Add some global error handling --- osu.Game/Screens/Multi/RoomManager.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 4d6ac46c84..b8c969a845 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -166,8 +166,16 @@ namespace osu.Game.Screens.Multi var r = listing[i]; r.Position.Value = i; - update(r, r); - addRoom(r); + try + { + update(r, r); + addRoom(r); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + rooms.Remove(r); + } } RoomsUpdated?.Invoke(); From e8d36bc3cbb5c6aa2c0f0fd6dde7d18d34799590 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 27 Jun 2020 00:19:22 +0900 Subject: [PATCH 083/100] Don't trigger the same exception multiple times --- osu.Game/Screens/Multi/RoomManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index b8c969a845..5083fb2ee3 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -142,6 +143,8 @@ namespace osu.Game.Screens.Multi joinedRoom = null; } + private readonly List roomsFailedUpdate = new List(); + /// /// Invoked when the listing of all s is received from the server. /// @@ -173,7 +176,14 @@ namespace osu.Game.Screens.Multi } catch (Exception ex) { - Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + Debug.Assert(r.RoomID.Value != null); + + if (!roomsFailedUpdate.Contains(r.RoomID.Value.Value)) + { + Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + roomsFailedUpdate.Add(r.RoomID.Value.Value); + } + rooms.Remove(r); } } From 3783fe8d6a2797925e4ad77525c676226fcf9bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:03:41 +0200 Subject: [PATCH 084/100] Rename fields for clarity --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 14 +++++------ osu.Game/Screens/Play/HUD/FailingLayer.cs | 23 ++++++++++--------- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 3eda47627b..1c55595c97 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; - private readonly Bindable enabledHUD = new Bindable(); + private readonly Bindable showHealth = new Bindable(); [Resolved] private OsuConfigManager config { get; set; } @@ -27,10 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = layer = new FailingLayer(); layer.BindHealthProcessor(new DrainingHealthProcessor(1)); - layer.HUDEnabled.BindTo(enabledHUD); + layer.ShowHealth.BindTo(showHealth); }); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -80,19 +80,19 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("set health to 0.10", () => layer.Current.Value = 0.1); - AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("don't show health", () => showHealth.Value = false); AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("don't show health", () => showHealth.Value = false); AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is visible", () => layer.IsPresent); } diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index d982764c30..e8c99c2ed8 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,11 +31,12 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; - public readonly Bindable HUDEnabled = new Bindable(); - private readonly Bindable enabled = new Bindable(); + public readonly Bindable ShowHealth = new Bindable(); + + private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; - private Bindable configEnabled; + private Bindable fadePlayfieldWhenHealthLowSetting; private HealthProcessor healthProcessor; public FailingLayer() @@ -74,9 +75,9 @@ namespace osu.Game.Screens.Play.HUD { boxes.Colour = color.Red; - configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); - HUDEnabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + fadePlayfieldWhenHealthLow.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + ShowHealth.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); } protected override void LoadComplete() @@ -98,13 +99,13 @@ namespace osu.Game.Screens.Play.HUD if (LoadState < LoadState.Ready) return; - enabled.UnbindBindings(); + fadePlayfieldWhenHealthLow.UnbindBindings(); // Don't display ever if the ruleset is not using a draining health display. if (healthProcessor is DrainingHealthProcessor) - enabled.BindTo(configEnabled); + fadePlayfieldWhenHealthLow.BindTo(fadePlayfieldWhenHealthLowSetting); else - enabled.Value = false; + fadePlayfieldWhenHealthLow.Value = false; } /// @@ -115,11 +116,11 @@ namespace osu.Game.Screens.Play.HUD /// True when you want to fade in, false when you want to fade out public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) { - if (HUDEnabled.Value) + if (ShowHealth.Value) { if (fadeIn) { - if (enabled.Value) + if (fadePlayfieldWhenHealthLow.Value) this.FadeIn(fadeDuration, easing); } else diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b55a93db1f..96e9625f76 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); - FailingLayer.HUDEnabled.BindTo(ShowHealthbar); + FailingLayer.ShowHealth.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); From 415e1c05ff7c83f9a2ef0f7981a80ecf24f60d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:06:41 +0200 Subject: [PATCH 085/100] Simplify implementation --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 26 +++++------------------ 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index e8c99c2ed8..22b7950d31 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -76,8 +76,8 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - fadePlayfieldWhenHealthLow.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); - ShowHealth.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState(), true); + ShowHealth.BindValueChanged(_ => updateState(), true); } protected override void LoadComplete() @@ -108,26 +108,10 @@ namespace osu.Game.Screens.Play.HUD fadePlayfieldWhenHealthLow.Value = false; } - /// - /// Tries to fade based on "Fade playfield when health is low" setting - /// - /// Duration of the fade - /// Type of easing - /// True when you want to fade in, false when you want to fade out - public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) + private void updateState() { - if (ShowHealth.Value) - { - if (fadeIn) - { - if (fadePlayfieldWhenHealthLow.Value) - this.FadeIn(fadeDuration, easing); - } - else - this.FadeOut(fadeDuration, easing); - } - else - this.FadeOut(fadeDuration, easing); + var showLayer = fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; + this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint); } protected override void Update() From a63b6a3ddf571bb941b858347fe903a4b82a1c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:22:30 +0200 Subject: [PATCH 086/100] Simplify binding --- osu.Game/Screens/Play/HUDOverlay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 96e9625f76..f09745cf71 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -153,8 +153,6 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); - - FailingLayer.ShowHealth.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); @@ -264,7 +262,10 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 } }; - protected virtual FailingLayer CreateFailingLayer() => new FailingLayer(); + protected virtual FailingLayer CreateFailingLayer() => new FailingLayer + { + ShowHealth = { BindTarget = ShowHealthbar } + }; protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay { From 02f590309d9b67c40e582a1c4f4302ee216204f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:22:45 +0200 Subject: [PATCH 087/100] Add xmldoc for public property --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 22b7950d31..d4faa4bbb7 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + /// + /// Whether the current player health should be shown on screen. + /// public readonly Bindable ShowHealth = new Bindable(); private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); From 3637bf2f9bc4929cd58cffb4aeb8830e1ceee690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:23:42 +0200 Subject: [PATCH 088/100] Clean up member order & access modifiers --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index d4faa4bbb7..b96cfd170e 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -18,10 +18,15 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { /// - /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by . + /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by . /// public class FailingLayer : HealthDisplay { + /// + /// Whether the current player health should be shown on screen. + /// + public readonly Bindable ShowHealth = new Bindable(); + private const float max_alpha = 0.4f; private const int fade_time = 400; private const float gradient_size = 0.3f; @@ -29,12 +34,7 @@ namespace osu.Game.Screens.Play.HUD /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// - public double LowHealthThreshold = 0.20f; - - /// - /// Whether the current player health should be shown on screen. - /// - public readonly Bindable ShowHealth = new Bindable(); + private const double low_health_threshold = 0.20f; private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { - double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); + double target = Math.Clamp(max_alpha * (1 - Current.Value / low_health_threshold), 0, max_alpha); boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f); From 082c94f98dfd7b00515846a06045e7b3949205b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Jun 2020 13:14:46 +0200 Subject: [PATCH 089/100] Temporarily disable masking of tournament song bar --- osu.Game.Tournament/Components/SongBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index fc7fcef892..cafec0a88b 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -66,6 +67,9 @@ namespace osu.Game.Tournament.Components } } + // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away. + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + [BackgroundDependencyLoader] private void load() { @@ -77,8 +81,6 @@ namespace osu.Game.Tournament.Components flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away. - Height = 1, AutoSizeAxes = Axes.Y, LayoutDuration = 500, LayoutEasing = Easing.OutQuint, From 006adf0fb50a903c67c7317b3c721ff653615506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 28 Jun 2020 22:45:13 +0900 Subject: [PATCH 090/100] Change logic to ignore rooms completely after first error --- osu.Game/Screens/Multi/RoomManager.cs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 5083fb2ee3..642378d8d5 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi joinedRoom = null; } - private readonly List roomsFailedUpdate = new List(); + private readonly HashSet ignoredRooms = new HashSet(); /// /// Invoked when the listing of all s is received from the server. @@ -166,25 +166,26 @@ namespace osu.Game.Screens.Multi continue; } - var r = listing[i]; - r.Position.Value = i; + var room = listing[i]; + + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) + continue; + + room.Position.Value = i; try { - update(r, r); - addRoom(r); + update(room, room); + addRoom(room); } catch (Exception ex) { - Debug.Assert(r.RoomID.Value != null); + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - if (!roomsFailedUpdate.Contains(r.RoomID.Value.Value)) - { - Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); - roomsFailedUpdate.Add(r.RoomID.Value.Value); - } - - rooms.Remove(r); + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); } } From 678767918e29dee961730f5f8f18a2a0e6e98c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Jun 2020 23:32:04 +0200 Subject: [PATCH 091/100] Centralise logic further --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 30 ++++++----------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index b96cfd170e..84dbb35f68 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -36,10 +36,9 @@ namespace osu.Game.Screens.Play.HUD /// private const double low_health_threshold = 0.20f; - private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; - private Bindable fadePlayfieldWhenHealthLowSetting; + private Bindable fadePlayfieldWhenHealthLow; private HealthProcessor healthProcessor; public FailingLayer() @@ -78,15 +77,15 @@ namespace osu.Game.Screens.Play.HUD { boxes.Colour = color.Red; - fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState(), true); - ShowHealth.BindValueChanged(_ => updateState(), true); + fadePlayfieldWhenHealthLow = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState()); + ShowHealth.BindValueChanged(_ => updateState()); } protected override void LoadComplete() { base.LoadComplete(); - updateBindings(); + updateState(); } public override void BindHealthProcessor(HealthProcessor processor) @@ -94,26 +93,13 @@ namespace osu.Game.Screens.Play.HUD base.BindHealthProcessor(processor); healthProcessor = processor; - updateBindings(); - } - - private void updateBindings() - { - if (LoadState < LoadState.Ready) - return; - - fadePlayfieldWhenHealthLow.UnbindBindings(); - - // Don't display ever if the ruleset is not using a draining health display. - if (healthProcessor is DrainingHealthProcessor) - fadePlayfieldWhenHealthLow.BindTo(fadePlayfieldWhenHealthLowSetting); - else - fadePlayfieldWhenHealthLow.Value = false; + updateState(); } private void updateState() { - var showLayer = fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; + // Don't display ever if the ruleset is not using a draining health display. + var showLayer = healthProcessor is DrainingHealthProcessor && fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint); } From ffbce61ca884320098351a331bcd658041fe79b2 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:39:49 +0200 Subject: [PATCH 092/100] Add the option to loop the intro in the main menu --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Overlays/Settings/Sections/Audio/MainMenuSettings.cs | 5 +++++ osu.Game/Screens/Menu/IntroScreen.cs | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..aa9b5340f6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -55,6 +55,7 @@ namespace osu.Game.Configuration Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); Set(OsuSetting.MenuVoice, true); + Set(OsuSetting.MenuMusicLoop, true); Set(OsuSetting.MenuMusic, true); Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -191,6 +192,7 @@ namespace osu.Game.Configuration AudioOffset, VolumeInactive, MenuMusic, + MenuMusicLoop, MenuVoice, CursorRotation, MenuParallax, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index a303f93b34..7ec123c04c 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -28,6 +28,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "osu! music theme", Bindable = config.GetBindable(OsuSetting.MenuMusic) }, + new SettingsCheckbox + { + LabelText = "loop the music theme", + Bindable = config.GetBindable(OsuSetting.MenuMusicLoop) + }, new SettingsDropdown { LabelText = "Intro sequence", diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 88d18d0073..57f93690a8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } + private IBindable menuMusicLoop { get; set; } private WorkingBeatmap initialBeatmap; @@ -73,6 +74,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); + menuMusicLoop = config.GetBindable(OsuSetting.MenuMusicLoop); seeya = audio.Samples.Get(SeeyaSampleName); @@ -152,6 +154,8 @@ namespace osu.Game.Screens.Menu // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. if (UsingThemedIntro) Track.Restart(); + if (menuMusicLoop.Value) + Track.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) From 5689f279871de69937a37342e10efcb98e5232e1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:54:06 +0200 Subject: [PATCH 093/100] Make sure it only loops for themed intros if true --- osu.Game/Screens/Menu/IntroScreen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 57f93690a8..fa8a641203 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -152,8 +152,10 @@ namespace osu.Game.Screens.Menu protected void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (UsingThemedIntro) - Track.Restart(); + if (!UsingThemedIntro) + return; + + Track.Restart(); if (menuMusicLoop.Value) Track.Looping = true; } From 270384e71e1fe41226eaf4864b6955fe5abcc4b1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:59:44 +0200 Subject: [PATCH 094/100] Remove redundant get set --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index fa8a641203..8ef7ebe5e6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } - private IBindable menuMusicLoop { get; set; } + private IBindable menuMusicLoop; private WorkingBeatmap initialBeatmap; From 24dceb9f84e49bf778ad575076f917065e6d5f67 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 01:41:47 +0200 Subject: [PATCH 095/100] Make only "Welcome" loop --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- .../Settings/Sections/Audio/MainMenuSettings.cs | 5 ----- osu.Game/Screens/Menu/IntroScreen.cs | 11 ++--------- osu.Game/Screens/Menu/IntroWelcome.cs | 2 ++ 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index aa9b5340f6..9d31bc9bba 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -55,7 +55,6 @@ namespace osu.Game.Configuration Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); Set(OsuSetting.MenuVoice, true); - Set(OsuSetting.MenuMusicLoop, true); Set(OsuSetting.MenuMusic, true); Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -192,7 +191,6 @@ namespace osu.Game.Configuration AudioOffset, VolumeInactive, MenuMusic, - MenuMusicLoop, MenuVoice, CursorRotation, MenuParallax, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index 7ec123c04c..a303f93b34 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -28,11 +28,6 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "osu! music theme", Bindable = config.GetBindable(OsuSetting.MenuMusic) }, - new SettingsCheckbox - { - LabelText = "loop the music theme", - Bindable = config.GetBindable(OsuSetting.MenuMusicLoop) - }, new SettingsDropdown { LabelText = "Intro sequence", diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 8ef7ebe5e6..5f91aaad15 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,7 +40,6 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } - private IBindable menuMusicLoop; private WorkingBeatmap initialBeatmap; @@ -74,8 +73,6 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - menuMusicLoop = config.GetBindable(OsuSetting.MenuMusicLoop); - seeya = audio.Samples.Get(SeeyaSampleName); BeatmapSetInfo setInfo = null; @@ -152,12 +149,8 @@ namespace osu.Game.Screens.Menu protected void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (!UsingThemedIntro) - return; - - Track.Restart(); - if (menuMusicLoop.Value) - Track.Looping = true; + if (UsingThemedIntro) + Track.Restart(); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index abd4a68d4f..bf42e36e8c 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Menu welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); + + Track.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) From 444504f2b9c7765d6219b643e1b10464b8810240 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 02:10:40 +0200 Subject: [PATCH 096/100] Expose MainMenu Track as internal get private set --- osu.Game/Screens/Menu/MainMenu.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f0da2482d6..9245df2a7d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -6,6 +6,7 @@ using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -63,6 +64,8 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => background; + internal Track Track { get; private set; } + private Bindable holdDelay; private Bindable loginDisplayed; @@ -173,15 +176,15 @@ namespace osu.Game.Screens.Menu base.OnEntering(last); buttons.FadeInFromZero(500); - var track = Beatmap.Value.Track; + Track = Beatmap.Value.Track; var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && track != null) + if (last is IntroScreen && Track != null) { - if (!track.IsRunning) + if (!Track.IsRunning) { - track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * track.Length); - track.Start(); + Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length); + Track.Start(); } } } From 0c4b06b48562fc15ef0ccdec53932ffaefb9d109 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 02:16:19 +0200 Subject: [PATCH 097/100] Add visualtest to check if Track loops in Welcome --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 12 ++++++------ .../Visual/Menus/TestSceneIntroWelcome.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 2d2f1a1618..f71d13ed35 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus [Cached] private OsuLogo logo; + protected OsuScreenStack IntroStack; + protected IntroTestScene() { - OsuScreenStack introStack = null; - Children = new Drawable[] { new Box @@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus logo.FinishTransforms(); logo.IsTracking = false; - introStack?.Expire(); + IntroStack?.Expire(); - Add(introStack = new OsuScreenStack + Add(IntroStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both, }); - introStack.Push(CreateScreen()); + IntroStack.Push(CreateScreen()); }); - AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu); + AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu); } protected abstract IScreen CreateScreen(); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 905f17ef0b..1347bae2ad 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -11,5 +11,18 @@ namespace osu.Game.Tests.Visual.Menus public class TestSceneIntroWelcome : IntroTestScene { protected override IScreen CreateScreen() => new IntroWelcome(); + + public TestSceneIntroWelcome() + { + AddAssert("check if menu music loops", () => + { + var menu = IntroStack?.CurrentScreen as MainMenu; + + if (menu == null) + return false; + + return menu.Track.Looping; + }); + } } } From 5db103dc613d238413b56ea3b2d31312b68e67cc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 29 Jun 2020 14:38:50 +0900 Subject: [PATCH 098/100] Improve quality of taiko hit target --- osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs index 7de1593ab6..caddc8b122 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), + Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, From bb81f908fb163f0d77d2d2fb74c54be563cbb859 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 29 Jun 2020 15:44:10 +0800 Subject: [PATCH 099/100] Exclude EmptyHitWindow from being considered in TimingDistributionGraph --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 8ec7e863b1..527da429ed 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents; + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows)).ToList(); } [BackgroundDependencyLoader] From 51f5083c2d71a87eb8fcda3f6aa1b1a748f86b47 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 17:17:52 +0000 Subject: [PATCH 100/100] Bump Sentry from 2.1.3 to 2.1.4 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.3 to 2.1.4. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.3...2.1.4) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 26d81a1004..5f326a361d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - +