From cabbc486e9d1940f4d95429c6691758ff7ee6b85 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:36:20 +0800 Subject: [PATCH 01/37] Rotate sliders in random mod --- .../Utils/OsuHitObjectGenerationUtils.cs | 35 +++++++++ .../OsuHitObjectGenerationUtils_Reposition.cs | 72 +++++++++++++++++-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index da73c2addb..19d3390f56 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -146,5 +146,40 @@ namespace osu.Game.Rulesets.Osu.Utils slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); } + + /// + /// Rotate a slider about its start position by the specified angle. + /// + /// The slider to be rotated. + /// The angle to rotate the slider by. + public static void RotateSlider(Slider slider, float rotation) + { + void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + + var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(); + foreach (var point in controlPoints) + point.Position = rotateVector(point.Position, rotation); + + slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); + } + + /// + /// Rotate a vector by the specified angle. + /// + /// The vector to be rotated. + /// The angle to rotate the vector by. + /// The rotated vector. + private static Vector2 rotateVector(Vector2 vector, float rotation) + { + float angle = (float)Math.Atan2(vector.Y, vector.X) + rotation; + float length = vector.Length; + return new Vector2( + length * (float)Math.Cos(angle), + length * (float)Math.Sin(angle) + ); + } } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index d1bc3b45df..ef1c258a8d 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -40,12 +40,21 @@ namespace osu.Game.Rulesets.Osu.Utils float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - positionInfos.Add(new ObjectPositionInfo(hitObject) + ObjectPositionInfo positionInfo; + positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length }); + if (hitObject is Slider) + { + var endPositionVector = hitObject.EndPosition - hitObject.Position; + float absoluteRotation = (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + positionInfo.Rotation = absoluteRotation - absoluteAngle; + absoluteAngle = absoluteRotation; + } + previousPosition = hitObject.EndPosition; previousAngle = absoluteAngle; } @@ -124,9 +133,16 @@ namespace osu.Game.Rulesets.Osu.Utils if (previous != null) { - Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; - Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + if (previous.HitObject is Slider s) + { + previousAbsoluteAngle = getSliderRotation(s); + } + else + { + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + } } float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; @@ -141,6 +157,16 @@ namespace osu.Game.Rulesets.Osu.Utils posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionModified = lastEndPosition + posRelativeToPrev; + + if (!(current.HitObject is Slider slider)) + return; + + Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); + Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation - current.RotationOriginal); + centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); + + float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); + RotateSlider(slider, relativeRotation); } /// @@ -287,6 +313,27 @@ namespace osu.Game.Rulesets.Osu.Utils ); } + private static Vector2 calculateCentreOfMass(Slider slider) + { + int count = 0; + Vector2 sum = Vector2.Zero; + double pathDistance = slider.Distance; + + for (double i = 0; i < pathDistance; i++) + { + sum += slider.Path.PositionAt(i / pathDistance); + count++; + } + + return sum / count; + } + + private static float getSliderRotation(Slider slider) + { + var endPositionVector = slider.EndPosition - slider.Position; + return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + } + public class ObjectPositionInfo { /// @@ -309,6 +356,13 @@ namespace osu.Game.Rulesets.Osu.Utils /// public float DistanceFromPrevious { get; set; } + /// + /// The rotation of the hit object, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to its end position, relative to its jump angle. + /// For hit circles and spinners, this property is ignored. + /// + public float Rotation { get; set; } + /// /// The hit object associated with this . /// @@ -325,6 +379,7 @@ namespace osu.Game.Rulesets.Osu.Utils public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } + public float RotationOriginal { get; } public ObjectPositionInfo PositionInfo { get; } public OsuHitObject HitObject => PositionInfo.HitObject; @@ -334,6 +389,15 @@ namespace osu.Game.Rulesets.Osu.Utils PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; + + if (HitObject is Slider slider) + { + RotationOriginal = getSliderRotation(slider); + } + else + { + RotationOriginal = 0; + } } } } From 998df5a4fef7627a897c23afa4f8b57c01e2a6f7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:37:10 +0800 Subject: [PATCH 02/37] Fix large slider clamping --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index ef1c258a8d..9f308df985 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -198,13 +198,13 @@ namespace osu.Game.Rulesets.Osu.Utils var previousPosition = workingObject.PositionModified; // Clamp slider position to the placement area - // If the slider is larger than the playfield, force it to stay at the original position + // If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield float newX = possibleMovementBounds.Width < 0 - ? workingObject.PositionOriginal.X + ? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X) : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); float newY = possibleMovementBounds.Height < 0 - ? workingObject.PositionOriginal.Y + ? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y) : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); slider.Position = workingObject.PositionModified = new Vector2(newX, newY); From af3835083ccd246810758b0156adbca2b3117587 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:41:45 +0800 Subject: [PATCH 03/37] Fix slider relative rotation calculation --- .../OsuHitObjectGenerationUtils_Reposition.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 9f308df985..fe5841daac 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -47,10 +47,9 @@ namespace osu.Game.Rulesets.Osu.Utils DistanceFromPrevious = relativePosition.Length }); - if (hitObject is Slider) + if (hitObject is Slider slider) { - var endPositionVector = hitObject.EndPosition - hitObject.Position; - float absoluteRotation = (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + float absoluteRotation = getSliderRotation(slider); positionInfo.Rotation = absoluteRotation - absoluteAngle; absoluteAngle = absoluteRotation; } @@ -161,8 +160,10 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(current.HitObject is Slider slider)) return; + absoluteAngle = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); - Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation - current.RotationOriginal); + Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider)); centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); @@ -379,7 +380,6 @@ namespace osu.Game.Rulesets.Osu.Utils public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } - public float RotationOriginal { get; } public ObjectPositionInfo PositionInfo { get; } public OsuHitObject HitObject => PositionInfo.HitObject; @@ -389,15 +389,6 @@ namespace osu.Game.Rulesets.Osu.Utils PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; - - if (HitObject is Slider slider) - { - RotationOriginal = getSliderRotation(slider); - } - else - { - RotationOriginal = 0; - } } } } From c0a78924aa2e5fe9ea9f6fe671e7fa770e472cac Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:47:21 +0800 Subject: [PATCH 04/37] Fix generation for zero-length sliders --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index fe5841daac..5f3719146f 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; @@ -167,7 +168,8 @@ namespace osu.Game.Rulesets.Osu.Utils centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); - RotateSlider(slider, relativeRotation); + if (!Precision.AlmostEquals(relativeRotation, 0)) + RotateSlider(slider, relativeRotation); } /// @@ -316,6 +318,8 @@ namespace osu.Game.Rulesets.Osu.Utils private static Vector2 calculateCentreOfMass(Slider slider) { + if (slider.Distance < 1) return Vector2.Zero; + int count = 0; Vector2 sum = Vector2.Zero; double pathDistance = slider.Distance; From 0015f627b04a23f407ff514255650003fb610c0b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:49:27 +0800 Subject: [PATCH 05/37] Add xmldoc --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 5f3719146f..ccc2529768 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -316,6 +316,11 @@ namespace osu.Game.Rulesets.Osu.Utils ); } + /// + /// Estimate the centre of mass of a slider relative to its start position. + /// + /// The slider to process. + /// The centre of mass of the slider. private static Vector2 calculateCentreOfMass(Slider slider) { if (slider.Distance < 1) return Vector2.Zero; @@ -333,6 +338,11 @@ namespace osu.Game.Rulesets.Osu.Utils return sum / count; } + /// + /// Get the absolute rotation of a slider, defined as the angle from its start position to its end position. + /// + /// The slider to process. + /// The angle in radians. private static float getSliderRotation(Slider slider) { var endPositionVector = slider.EndPosition - slider.Position; From 031a977009d466796eb90aa9386d24ce16410f6c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:50:30 +0800 Subject: [PATCH 06/37] Calculate slider rotation using end point of path instead of EndPosition --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index ccc2529768..45285e5e0c 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -339,13 +339,13 @@ namespace osu.Game.Rulesets.Osu.Utils } /// - /// Get the absolute rotation of a slider, defined as the angle from its start position to its end position. + /// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path. /// /// The slider to process. /// The angle in radians. private static float getSliderRotation(Slider slider) { - var endPositionVector = slider.EndPosition - slider.Position; + var endPositionVector = slider.Path.PositionAt(1); return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); } @@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// /// The rotation of the hit object, relative to its jump angle. - /// For sliders, this is defined as the angle from the slider's start position to its end position, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle. /// For hit circles and spinners, this property is ignored. /// public float Rotation { get; set; } From ee6567788425bd24b198493bdd7f565fd6f0b4a5 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:57:45 +0800 Subject: [PATCH 07/37] Use height of playfield instead of width when randomizing the first object This is the change discussed in #17194. The effect of this change is barely noticeable, but it makes more sense to generate the object within playfield from the start. --- osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index fea9246035..ccc56bd64f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (positionInfo == positionInfos.First()) { - positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); + positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else From 3bebc88306c9abb2543686ba22fbbcbd51d2b0da Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 1 Apr 2022 11:59:24 +0800 Subject: [PATCH 08/37] Consider spinners when calculating jump angles Spinners are considered in `GeneratePositionInfos`, so they should also be considered in `RepositionHitObjects` --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 45285e5e0c..664bfae35a 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (hitObject is Spinner) { - previous = null; + previous = current; continue; } From 72cb3d6ad63dc0967a74980d67ae3c097c02e41b Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 11 Apr 2022 14:15:08 +0800 Subject: [PATCH 09/37] USe `MathF` in all applicable places --- .../Utils/OsuHitObjectGenerationUtils.cs | 6 +++--- .../OsuHitObjectGenerationUtils_Reposition.cs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 19d3390f56..6129e6bfc4 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -174,11 +174,11 @@ namespace osu.Game.Rulesets.Osu.Utils /// The rotated vector. private static Vector2 rotateVector(Vector2 vector, float rotation) { - float angle = (float)Math.Atan2(vector.Y, vector.X) + rotation; + float angle = MathF.Atan2(vector.Y, vector.X) + rotation; float length = vector.Length; return new Vector2( - length * (float)Math.Cos(angle), - length * (float)Math.Sin(angle) + length * MathF.Cos(angle), + length * MathF.Sin(angle) ); } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 664bfae35a..2abbd61c59 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Utils foreach (OsuHitObject hitObject in hitObjects) { Vector2 relativePosition = hitObject.Position - previousPosition; - float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; ObjectPositionInfo positionInfo; @@ -141,15 +141,15 @@ namespace osu.Game.Rulesets.Osu.Utils { Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); } } float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; var posRelativeToPrev = new Vector2( - current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), - current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) + current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle), + current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; @@ -161,13 +161,13 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(current.HitObject is Slider slider)) return; - absoluteAngle = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider)); centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); - float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); + float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); if (!Precision.AlmostEquals(relativeRotation, 0)) RotateSlider(slider, relativeRotation); } @@ -346,7 +346,7 @@ namespace osu.Game.Rulesets.Osu.Utils private static float getSliderRotation(Slider slider) { var endPositionVector = slider.Path.PositionAt(1); - return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + return MathF.Atan2(endPositionVector.Y, endPositionVector.X); } public class ObjectPositionInfo From 610d2dc1a310d918604f758705bab41b8d2af451 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 17 Apr 2022 10:34:48 +0800 Subject: [PATCH 10/37] Use a bigger sample step to calculate slider center of mass --- .../Utils/OsuHitObjectGenerationUtils_Reposition.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index 2abbd61c59..a77d1f8b0f 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -323,13 +323,19 @@ namespace osu.Game.Rulesets.Osu.Utils /// The centre of mass of the slider. private static Vector2 calculateCentreOfMass(Slider slider) { - if (slider.Distance < 1) return Vector2.Zero; + const double sample_step = 50; + + // just sample the start and end positions if the slider is too short + if (slider.Distance <= sample_step) + { + return Vector2.Divide(slider.Path.PositionAt(1), 2); + } int count = 0; Vector2 sum = Vector2.Zero; double pathDistance = slider.Distance; - for (double i = 0; i < pathDistance; i++) + for (double i = 0; i < pathDistance; i += sample_step) { sum += slider.Path.PositionAt(i / pathDistance); count++; From 1d79266d422fe8dbe06eec2d259c2aab348df106 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 17 Apr 2022 10:40:43 +0800 Subject: [PATCH 11/37] Clarify in the xmldoc that angles are measured in radians --- osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 6129e6bfc4..7b0d061e9a 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Rotate a slider about its start position by the specified angle. /// /// The slider to be rotated. - /// The angle to rotate the slider by. + /// The angle, measured in radians, to rotate the slider by. public static void RotateSlider(Slider slider, float rotation) { void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Utils /// Rotate a vector by the specified angle. /// /// The vector to be rotated. - /// The angle to rotate the vector by. + /// The angle, measured in radians, to rotate the vector by. /// The rotated vector. private static Vector2 rotateVector(Vector2 vector, float rotation) { From e5e196097584484bc9062328b887cd806232b960 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 18 Apr 2022 09:38:51 +0800 Subject: [PATCH 12/37] Add inline comments --- osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index 7b0d061e9a..266f7d1251 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); @@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); @@ -156,6 +158,7 @@ namespace osu.Game.Rulesets.Osu.Utils { void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); From 3183621d3d20e05d65d4f6b9ea383d7cc6df3512 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Thu, 26 May 2022 22:57:24 +0200 Subject: [PATCH 13/37] Add search bar for the `CurrentlyPlayingDisplay` --- .../Dashboard/CurrentlyPlayingDisplay.cs | 119 ++++++++++++++---- 1 file changed, 98 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index a9312e9a3a..1c14eee9c7 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.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.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -11,6 +12,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; @@ -24,10 +27,17 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { + private const float search_bar_height = 40; + private const float search_bar_width = 250; + private readonly IBindableList playingUsers = new BindableList(); private FillFlowContainer userFlow; + private List currentUsers = new List(); + private FocusedTextBox searchBar; + private Container searchBarContainer; + [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -37,13 +47,46 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = userFlow = new FillFlowContainer + searchBarContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding(10), + Child = searchBar = new FocusedTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = search_bar_height, + Width = search_bar_width, + + Colour = OsuColour.Gray(0.8f), + + PlaceholderText = "Search for User...", + HoldFocus = true, + ReleaseFocusOnCommit = true, + }, + }; + + userFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), + Padding = new MarginPadding { + Top = 10 + 10 + search_bar_height, + Bottom = 10, + Right = 10, + Left = 10, + }, Spacing = new Vector2(10), }; + + InternalChildren = new Drawable[] + { + searchBarContainer, + userFlow, + }; + + searchBar.Current.ValueChanged += onSearchBarValueChanged; } [Resolved] @@ -57,6 +100,57 @@ namespace osu.Game.Overlays.Dashboard playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } + private void addCard(int userId) + { + if (currentUsers.Contains(userId)) + return; + + users.GetUserAsync(userId).ContinueWith(task => + { + var user = task.GetResultSafely(); + + if (user == null) + return; + + if (!user.Username.ToLower().Contains(searchBar.Text.ToLower())) + return; + + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + currentUsers.Add(user.Id); + + userFlow.Add(createUserPanel(user)); + }); + }); + } + + private void removeCard(PlayingUserPanel card) + { + if (card == null) + return; + + currentUsers.Remove(card.User.Id); + card.Expire(); + } + + private void onSearchBarValueChanged(ValueChangedEvent change) + { + foreach (PlayingUserPanel card in userFlow) + { + if (!card.User.Username.ToLower().Contains(change.NewValue.ToLower())) + removeCard(card); + } + + foreach (int userId in playingUsers) + { + addCard(userId); + } + } + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) @@ -65,24 +159,7 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.NewItems != null); foreach (int userId in e.NewItems) - { - users.GetUserAsync(userId).ContinueWith(task => - { - var user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); - }); - } + addCard(userId); break; @@ -90,7 +167,7 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach (int userId in e.OldItems) - userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); + removeCard(userFlow.FirstOrDefault(card => card.User.Id == userId)); break; } }); From de5b60ab66a6d33655e7b003c1ff21caa87867ad Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 00:40:23 +0200 Subject: [PATCH 14/37] Move filtering from manual loops to `SearchContainer` --- .../Dashboard/CurrentlyPlayingDisplay.cs | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 1c14eee9c7..7dd5c73b79 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Graphics; @@ -32,9 +33,8 @@ namespace osu.Game.Overlays.Dashboard private readonly IBindableList playingUsers = new BindableList(); - private FillFlowContainer userFlow; + private SearchContainer userFlow; - private List currentUsers = new List(); private FocusedTextBox searchBar; private Container searchBarContainer; @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Dashboard }, }; - userFlow = new FillFlowContainer + userFlow = new SearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -102,9 +102,6 @@ namespace osu.Game.Overlays.Dashboard private void addCard(int userId) { - if (currentUsers.Contains(userId)) - return; - users.GetUserAsync(userId).ContinueWith(task => { var user = task.GetResultSafely(); @@ -121,35 +118,12 @@ namespace osu.Game.Overlays.Dashboard if (!playingUsers.Contains(user.Id)) return; - currentUsers.Add(user.Id); - userFlow.Add(createUserPanel(user)); }); }); } - private void removeCard(PlayingUserPanel card) - { - if (card == null) - return; - - currentUsers.Remove(card.User.Id); - card.Expire(); - } - - private void onSearchBarValueChanged(ValueChangedEvent change) - { - foreach (PlayingUserPanel card in userFlow) - { - if (!card.User.Username.ToLower().Contains(change.NewValue.ToLower())) - removeCard(card); - } - - foreach (int userId in playingUsers) - { - addCard(userId); - } - } + private void onSearchBarValueChanged(ValueChangedEvent change) => userFlow.SearchTerm = searchBar.Text; private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { @@ -167,7 +141,7 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.OldItems != null); foreach (int userId in e.OldItems) - removeCard(userFlow.FirstOrDefault(card => card.User.Id == userId)); + userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire(); break; } }); @@ -179,16 +153,35 @@ namespace osu.Game.Overlays.Dashboard panel.Origin = Anchor.TopCentre; }); - private class PlayingUserPanel : CompositeDrawable + private class PlayingUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; + public IEnumerable FilterTerms => filterTerm; [Resolved(canBeNull: true)] private IPerformFromScreenRunner performer { get; set; } + private IEnumerable filterTerm; + + public bool MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + + public bool FilteringActive + { + set { } + } public PlayingUserPanel(APIUser user) { User = user; + filterTerm = new LocalisableString[] { User.Username }; AutoSizeAxes = Axes.Both; } From a8e453e6601f495613b11ebbe42c635113cf4d8d Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 00:43:29 +0200 Subject: [PATCH 15/37] Remove now unnecessary username filter check --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 7dd5c73b79..f57c808144 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -109,9 +109,6 @@ namespace osu.Game.Overlays.Dashboard if (user == null) return; - if (!user.Username.ToLower().Contains(searchBar.Text.ToLower())) - return; - Schedule(() => { // user may no longer be playing. From 4967d036063f954cdd37ac33134288ebfb26a881 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 09:58:40 +0200 Subject: [PATCH 16/37] Update currently playing search bar to resemble existing UI --- .../Dashboard/CurrentlyPlayingDisplay.cs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index f57c808144..b6deae27e5 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -10,6 +10,8 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; @@ -35,35 +37,42 @@ namespace osu.Game.Overlays.Dashboard private SearchContainer userFlow; - private FocusedTextBox searchBar; - private Container searchBarContainer; + private Box searchBarBackground; + private BasicSearchTextBox searchBar; + private Container searchBarContainer; [Resolved] private SpectatorClient spectatorClient { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - searchBarContainer = new Container + searchBarBackground = new Box + { + RelativeSizeAxes = Axes.X, + Height = 10*2 + search_bar_height, + Colour = colourProvider.Background4, + }; + + searchBarContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, Padding = new MarginPadding(10), - Child = searchBar = new FocusedTextBox + + Child = searchBar = new BasicSearchTextBox { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Height = search_bar_height, - Width = search_bar_width, - Colour = OsuColour.Gray(0.8f), + RelativeSizeAxes = Axes.X, - PlaceholderText = "Search for User...", - HoldFocus = true, - ReleaseFocusOnCommit = true, + PlaceholderText = "type to search", }, }; @@ -72,7 +81,7 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Top = 10 + 10 + search_bar_height, + Top = 10*3 + search_bar_height, Bottom = 10, Right = 10, Left = 10, @@ -82,6 +91,7 @@ namespace osu.Game.Overlays.Dashboard InternalChildren = new Drawable[] { + searchBarBackground, searchBarContainer, userFlow, }; @@ -170,10 +180,7 @@ namespace osu.Game.Overlays.Dashboard } } - public bool FilteringActive - { - set { } - } + public bool FilteringActive { set; get; } public PlayingUserPanel(APIUser user) { From 7c459faaf0bd9978f44ec6b2436b08818561866b Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 10:05:59 +0200 Subject: [PATCH 17/37] Remove unneeded search_bar_width and more --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index b6deae27e5..c7b08a1ffc 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Screens; @@ -30,8 +29,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private const float search_bar_height = 40; - private const float search_bar_width = 250; + private const float SEARCHBAR_HEIGHT = 40; private readonly IBindableList playingUsers = new BindableList(); @@ -53,7 +51,7 @@ namespace osu.Game.Overlays.Dashboard searchBarBackground = new Box { RelativeSizeAxes = Axes.X, - Height = 10*2 + search_bar_height, + Height = 10*2 + SEARCHBAR_HEIGHT, Colour = colourProvider.Background4, }; @@ -68,7 +66,7 @@ namespace osu.Game.Overlays.Dashboard { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Height = search_bar_height, + Height = SEARCHBAR_HEIGHT, RelativeSizeAxes = Axes.X, @@ -81,7 +79,7 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Top = 10*3 + search_bar_height, + Top = 10*3 + SEARCHBAR_HEIGHT, Bottom = 10, Right = 10, Left = 10, From 6a0cf26722cfa9ab08de4f8f00156657c798afbd Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 12:36:43 +0200 Subject: [PATCH 18/37] Make text localisable and add padding constant --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index c7b08a1ffc..d9418a303e 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Dashboard internal class CurrentlyPlayingDisplay : CompositeDrawable { private const float SEARCHBAR_HEIGHT = 40; + private const float CONTAINER_PADDING = 10; private readonly IBindableList playingUsers = new BindableList(); @@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Dashboard searchBarBackground = new Box { RelativeSizeAxes = Axes.X, - Height = 10*2 + SEARCHBAR_HEIGHT, + Height = CONTAINER_PADDING * 2 + SEARCHBAR_HEIGHT, Colour = colourProvider.Background4, }; @@ -70,7 +71,7 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X, - PlaceholderText = "type to search", + PlaceholderText = Resources.Localisation.Web.HomeStrings.SearchPlaceholder, }, }; @@ -79,7 +80,7 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Top = 10*3 + SEARCHBAR_HEIGHT, + Top = CONTAINER_PADDING * 3 + SEARCHBAR_HEIGHT, Bottom = 10, Right = 10, Left = 10, From e1fd37fd4481a7bbee4e2bd20e4a65da943382f3 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 12:40:31 +0200 Subject: [PATCH 19/37] Use new padding constant where appropriate --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index d9418a303e..a9e9932b5b 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(10), + Padding = new MarginPadding(CONTAINER_PADDING), Child = searchBar = new BasicSearchTextBox { @@ -81,9 +81,9 @@ namespace osu.Game.Overlays.Dashboard AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Top = CONTAINER_PADDING * 3 + SEARCHBAR_HEIGHT, - Bottom = 10, - Right = 10, - Left = 10, + Bottom = CONTAINER_PADDING, + Right = CONTAINER_PADDING, + Left = CONTAINER_PADDING, }, Spacing = new Vector2(10), }; From e2951d70d1eac5ee0f2291c2192ac9d8f8598269 Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 16:38:54 +0200 Subject: [PATCH 20/37] Address code style issues --- .../TestSceneCurrentlyPlayingDisplay.cs | 4 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 143 ++++++++---------- 2 files changed, 68 insertions(+), 79 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 35a4f8cf2d..edee26c081 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; +using osu.Game.Overlays; using osu.Game.Overlays.Dashboard; using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; @@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online CachedDependencies = new (Type, object)[] { (typeof(SpectatorClient), spectatorClient), - (typeof(UserLookupCache), lookupCache) + (typeof(UserLookupCache), lookupCache), + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)), }, Child = currentlyPlaying = new CurrentlyPlayingDisplay { diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index a9e9932b5b..3020b37351 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -29,16 +28,13 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private const float SEARCHBAR_HEIGHT = 40; - private const float CONTAINER_PADDING = 10; + private const float searchbox_height = 40f; + private const float container_padding = 10f; private readonly IBindableList playingUsers = new BindableList(); private SearchContainer userFlow; - - private Box searchBarBackground; - private BasicSearchTextBox searchBar; - private Container searchBarContainer; + private BasicSearchTextBox userFlowSearchTextBox; [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -49,53 +45,48 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - searchBarBackground = new Box - { - RelativeSizeAxes = Axes.X, - Height = CONTAINER_PADDING * 2 + SEARCHBAR_HEIGHT, - Colour = colourProvider.Background4, - }; - - searchBarContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding(CONTAINER_PADDING), - - Child = searchBar = new BasicSearchTextBox - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Height = SEARCHBAR_HEIGHT, - - RelativeSizeAxes = Axes.X, - - PlaceholderText = Resources.Localisation.Web.HomeStrings.SearchPlaceholder, - }, - }; - - userFlow = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { - Top = CONTAINER_PADDING * 3 + SEARCHBAR_HEIGHT, - Bottom = CONTAINER_PADDING, - Right = CONTAINER_PADDING, - Left = CONTAINER_PADDING, - }, - Spacing = new Vector2(10), - }; - InternalChildren = new Drawable[] { - searchBarBackground, - searchBarContainer, - userFlow, + new Box + { + RelativeSizeAxes = Axes.X, + Height = container_padding * 2 + searchbox_height, + Colour = colourProvider.Background4, + }, + + new Container + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding(container_padding), + + Child = userFlowSearchTextBox = new BasicSearchTextBox + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = searchbox_height, + PlaceholderText = Resources.Localisation.Web.HomeStrings.SearchPlaceholder, + }, + }, + + userFlow = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = container_padding * 3 + searchbox_height, + Bottom = container_padding, + Right = container_padding, + Left = container_padding, + }, + Spacing = new Vector2(10), + }, }; - searchBar.Current.ValueChanged += onSearchBarValueChanged; + userFlowSearchTextBox.Current.ValueChanged += onSearchBarValueChanged; } [Resolved] @@ -109,27 +100,7 @@ namespace osu.Game.Overlays.Dashboard playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void addCard(int userId) - { - users.GetUserAsync(userId).ContinueWith(task => - { - var user = task.GetResultSafely(); - - if (user == null) - return; - - Schedule(() => - { - // user may no longer be playing. - if (!playingUsers.Contains(user.Id)) - return; - - userFlow.Add(createUserPanel(user)); - }); - }); - } - - private void onSearchBarValueChanged(ValueChangedEvent change) => userFlow.SearchTerm = searchBar.Text; + private void onSearchBarValueChanged(ValueChangedEvent change) => userFlow.SearchTerm = userFlowSearchTextBox.Text; private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { @@ -139,7 +110,24 @@ namespace osu.Game.Overlays.Dashboard Debug.Assert(e.NewItems != null); foreach (int userId in e.NewItems) - addCard(userId); + { + users.GetUserAsync(userId).ContinueWith(task => + { + var user = task.GetResultSafely(); + + if (user == null) + return; + + Schedule(() => + { + // user may no longer be playing. + if (!playingUsers.Contains(user.Id)) + return; + + userFlow.Add(createUserPanel(user)); + }); + }); + } break; @@ -159,14 +147,15 @@ namespace osu.Game.Overlays.Dashboard panel.Origin = Anchor.TopCentre; }); - private class PlayingUserPanel : CompositeDrawable, IFilterable + public class PlayingUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; - public IEnumerable FilterTerms => filterTerm; + public IEnumerable FilterTerms { get; set; } [Resolved(canBeNull: true)] private IPerformFromScreenRunner performer { get; set; } - private IEnumerable filterTerm; + + public bool FilteringActive { set; get; } public bool MatchingFilter { @@ -179,12 +168,10 @@ namespace osu.Game.Overlays.Dashboard } } - public bool FilteringActive { set; get; } - public PlayingUserPanel(APIUser user) { User = user; - filterTerm = new LocalisableString[] { User.Username }; + FilterTerms = new LocalisableString[] { User.Username }; AutoSizeAxes = Axes.Both; } From 254039b8fe9fa21ae584663b792aa7b81d650a29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 May 2022 04:01:51 +0900 Subject: [PATCH 21/37] Address remaining code quality concerns --- .../Dashboard/CurrentlyPlayingDisplay.cs | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 3020b37351..0a1cf5524f 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; +using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; @@ -28,13 +29,13 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private const float searchbox_height = 40f; - private const float container_padding = 10f; + private const float search_textbox_height = 40; + private const float padding = 10; private readonly IBindableList playingUsers = new BindableList(); private SearchContainer userFlow; - private BasicSearchTextBox userFlowSearchTextBox; + private BasicSearchTextBox searchTextBox; [Resolved] private SpectatorClient spectatorClient { get; set; } @@ -50,43 +51,38 @@ namespace osu.Game.Overlays.Dashboard new Box { RelativeSizeAxes = Axes.X, - Height = container_padding * 2 + searchbox_height, + Height = padding * 2 + search_textbox_height, Colour = colourProvider.Background4, }, - new Container { RelativeSizeAxes = Axes.X, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(container_padding), - - Child = userFlowSearchTextBox = new BasicSearchTextBox + Padding = new MarginPadding(padding), + Child = searchTextBox = new BasicSearchTextBox { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Height = searchbox_height, - PlaceholderText = Resources.Localisation.Web.HomeStrings.SearchPlaceholder, + Height = search_textbox_height, + PlaceholderText = HomeStrings.SearchPlaceholder, }, }, - userFlow = new SearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), Padding = new MarginPadding { - Top = container_padding * 3 + searchbox_height, - Bottom = container_padding, - Right = container_padding, - Left = container_padding, + Top = padding * 3 + search_textbox_height, + Bottom = padding, + Right = padding, + Left = padding, }, - Spacing = new Vector2(10), }, }; - userFlowSearchTextBox.Current.ValueChanged += onSearchBarValueChanged; + searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue; } [Resolved] @@ -100,8 +96,6 @@ namespace osu.Game.Overlays.Dashboard playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } - private void onSearchBarValueChanged(ValueChangedEvent change) => userFlow.SearchTerm = userFlowSearchTextBox.Text; - private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) @@ -150,7 +144,8 @@ namespace osu.Game.Overlays.Dashboard public class PlayingUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; - public IEnumerable FilterTerms { get; set; } + + public IEnumerable FilterTerms { get; } [Resolved(canBeNull: true)] private IPerformFromScreenRunner performer { get; set; } @@ -171,6 +166,7 @@ namespace osu.Game.Overlays.Dashboard public PlayingUserPanel(APIUser user) { User = user; + FilterTerms = new LocalisableString[] { User.Username }; AutoSizeAxes = Axes.Both; From cd84200ad9486ad9b4f1e62941244750a1dc593b Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Fri, 27 May 2022 21:23:37 +0200 Subject: [PATCH 22/37] Add HoldFocus and ReleaseFocusOnCommit attributes to searchbar --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 0a1cf5524f..c8c53ec507 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -64,6 +64,8 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Height = search_textbox_height, + ReleaseFocusOnCommit = false, + HoldFocus = true, PlaceholderText = HomeStrings.SearchPlaceholder, }, }, From ede3ab9dc00692559786b6e5691e07d8af3c12ee Mon Sep 17 00:00:00 2001 From: sw1tchbl4d3 Date: Sat, 28 May 2022 12:04:25 +0200 Subject: [PATCH 23/37] Add OnFocus handler to CurrentlyPlayingDisplay --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 8 ++++++++ osu.Game/Overlays/DashboardOverlay.cs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index c8c53ec507..23f67a06cb 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Screens; using osu.Game.Database; @@ -98,6 +99,13 @@ namespace osu.Game.Overlays.Dashboard playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + searchTextBox.TakeFocus(); + } + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 83ad8faf1c..79d972bdcc 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -16,6 +16,8 @@ namespace osu.Game.Overlays protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader(); + public override bool AcceptsFocus => false; + protected override void CreateDisplayToLoad(DashboardOverlayTabs tab) { switch (tab) From 82a1ba1d46650bf11164e26537ddfa64ae995c84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 May 2022 17:42:27 +0900 Subject: [PATCH 24/37] Use pooled memory for memory copies performed by `ZipArchiveReader` --- osu.Game/IO/Archives/ZipArchiveReader.cs | 56 +++++++++++++++++++++--- osu.Game/osu.Game.csproj | 1 + 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 80dfa104f3..ae2b85da51 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -1,11 +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.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; +using SixLabors.ImageSharp.Memory; namespace osu.Game.IO.Archives { @@ -27,15 +31,12 @@ namespace osu.Game.IO.Archives if (entry == null) throw new FileNotFoundException(); - // allow seeking - MemoryStream copy = new MemoryStream(); + var owner = MemoryAllocator.Default.Allocate((int)entry.Size); using (Stream s = entry.OpenEntryStream()) - s.CopyTo(copy); + s.ReadToFill(owner.Memory.Span); - copy.Position = 0; - - return copy; + return new MemoryOwnerMemoryStream(owner); } public override void Dispose() @@ -45,5 +46,48 @@ namespace osu.Game.IO.Archives } public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); + + private class MemoryOwnerMemoryStream : Stream + { + private readonly IMemoryOwner owner; + private readonly Stream stream; + + public MemoryOwnerMemoryStream(IMemoryOwner owner) + { + this.owner = owner; + + stream = owner.Memory.AsStream(); + } + + protected override void Dispose(bool disposing) + { + owner?.Dispose(); + base.Dispose(disposing); + } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + + public override void SetLength(long value) => stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); + + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position + { + get => stream.Position; + set => stream.Position = value; + } + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 79cfd7c917..6ce21ccad6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,6 +29,7 @@ + all From d6b18d87b8f7c14bb078dca1863a4283ee16cb7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 May 2022 19:58:47 +0900 Subject: [PATCH 25/37] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index c28085557e..116c7dbfcd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6ce21ccad6..eb47d0468f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index b1ba64beba..ccecad6f82 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 132c94c1b57ce391c48a1446b29ab606ca61789f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 17:16:23 +0900 Subject: [PATCH 26/37] Remove Ruleset parameter from ResetFromReplayFrame() --- .../Gameplay/TestSceneScoreProcessor.cs | 6 +++--- .../Rulesets/Scoring/JudgementProcessor.cs | 3 +-- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 18 +++++++----------- osu.Game/Rulesets/UI/RulesetInputManager.cs | 6 +----- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index 9c307341bd..af4b002bc9 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -61,13 +61,13 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); // No header shouldn't cause any change - scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame()); + scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame()); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000)); Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); // Reset with a miss instead. - scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame { Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now) }); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1)); // Reset with no judged hit. - scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame + scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame { Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now) }); diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 94ddc32bb7..bfa67b8c45 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring /// /// If the provided replay frame does not have any header information, this will be a noop. /// - /// The ruleset to be used for retrieving statistics. /// The replay frame to read header statistics from. - public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + public virtual void ResetFromReplayFrame(ReplayFrame frame) { if (frame.Header == null) return; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1dd1d1aeb6..1403a3f243 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -251,8 +251,7 @@ namespace osu.Game.Rulesets.Scoring if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - extractFromStatistics(ruleset, - scoreInfo.Statistics, + extractFromStatistics(scoreInfo.Statistics, out double extractedBaseScore, out double extractedMaxBaseScore, out int extractedMaxCombo, @@ -281,8 +280,7 @@ namespace osu.Game.Rulesets.Scoring if (!beatmapApplied) throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); - extractFromStatistics(ruleset, - scoreInfo.Statistics, + extractFromStatistics(scoreInfo.Statistics, out double extractedBaseScore, out _, out _, @@ -318,9 +316,7 @@ namespace osu.Game.Rulesets.Scoring // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3) { - extractFromStatistics( - ruleset, - scoreInfo.Statistics, + extractFromStatistics(scoreInfo.Statistics, out double computedBaseScore, out double computedMaxBaseScore, out _, @@ -437,14 +433,14 @@ namespace osu.Game.Rulesets.Scoring score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score)); } - public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame) + public override void ResetFromReplayFrame(ReplayFrame frame) { - base.ResetFromReplayFrame(ruleset, frame); + base.ResetFromReplayFrame(frame); if (frame.Header == null) return; - extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _); + extractFromStatistics(frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _); HighestCombo.Value = frame.Header.MaxCombo; scoreResultCounts.Clear(); @@ -455,7 +451,7 @@ namespace osu.Game.Rulesets.Scoring OnResetFromReplayFrame?.Invoke(); } - private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo, + private void extractFromStatistics(IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo, out int basicHitObjects) { baseScore = 0; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7d1b23f48b..b5390eb6e2 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI { public readonly KeyBindingContainer KeyBindingContainer; - private readonly Ruleset ruleset; - [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } @@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) { - this.ruleset = ruleset.CreateInstance(); - InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique) .WithChild(content = new Container { RelativeSizeAxes = Axes.Both }); @@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI break; case ReplayStatisticsFrameEvent statisticsStateChangeEvent: - scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame); + scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame); break; default: From af0f934e1a1b47080aefb8f4353a411c3e20c732 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 17:37:48 +0900 Subject: [PATCH 27/37] Move raw ScoreProcessor values into ScoringValues struct --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 111 +++++++++++++------- osu.Game/Scoring/ScoringValues.cs | 38 +++++++ 2 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 osu.Game/Scoring/ScoringValues.cs diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1403a3f243..493da763cb 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -88,17 +88,20 @@ namespace osu.Game.Rulesets.Scoring private readonly double accuracyPortion; private readonly double comboPortion; - private int maxAchievableCombo; + /// + /// Maximum achievable scoring values. + /// + private ScoringValues maximumScoringValues; /// - /// The maximum achievable base score. + /// Maximum achievable scoring values up to the current point in time. /// - private double maxBaseScore; + private ScoringValues rollingMaximumScoringValues; /// - /// The maximum number of basic (non-tick and non-bonus) hitobjects. + /// Scoring values for the current play. /// - private int maxBasicHitObjects; + private ScoringValues currentScoringValues; /// /// The maximum of a basic (non-tick and non-bonus) hitobject. @@ -106,9 +109,6 @@ namespace osu.Game.Rulesets.Scoring /// private HitResult? maxBasicResult; - private double rollingMaxBaseScore; - private double baseScore; - private int basicHitObjects; private bool beatmapApplied; private readonly Dictionary scoreResultCounts = new Dictionary(); @@ -164,23 +164,42 @@ namespace osu.Game.Rulesets.Scoring scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1; if (!result.Type.IsScorable()) - return; + { + // The inverse of non-scorable (ignore) judgements may be bonus judgements. + if (result.Judgement.MaxResult.IsBonus()) + rollingMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; + return; + } + + // Update rolling combo. if (result.Type.IncreasesCombo()) Combo.Value++; else if (result.Type.BreaksCombo()) Combo.Value = 0; - double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + // Update maximum combo. + currentScoringValues.MaxCombo = HighestCombo.Value; + rollingMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; - if (!result.Type.IsBonus()) + // Update base/bonus score. + if (result.Type.IsBonus()) { - baseScore += scoreIncrease; - rollingMaxBaseScore += result.Judgement.MaxNumericResult; + currentScoringValues.BonusScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + rollingMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; + } + else + { + currentScoringValues.BaseScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + rollingMaximumScoringValues.BaseScore += result.Judgement.MaxNumericResult; } + // Update hitobject count. if (result.Type.IsBasic()) - basicHitObjects++; + { + currentScoringValues.HitObjects++; + rollingMaximumScoringValues.HitObjects++; + } hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -207,18 +226,36 @@ namespace osu.Game.Rulesets.Scoring scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1; if (!result.Type.IsScorable()) - return; - - double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - - if (!result.Type.IsBonus()) { - baseScore -= scoreIncrease; - rollingMaxBaseScore -= result.Judgement.MaxNumericResult; + // The inverse of non-scorable (ignore) judgements may be bonus judgements. + if (result.Judgement.MaxResult.IsBonus()) + rollingMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; + + return; } + // Update maximum combo. + currentScoringValues.MaxCombo = HighestCombo.Value; + rollingMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; + + // Update base/bonus score. + if (result.Type.IsBonus()) + { + currentScoringValues.BonusScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + rollingMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; + } + else + { + currentScoringValues.BaseScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + rollingMaximumScoringValues.BaseScore -= result.Judgement.MaxNumericResult; + } + + // Update hitobject count. if (result.Type.IsBasic()) - basicHitObjects--; + { + currentScoringValues.HitObjects--; + rollingMaximumScoringValues.HitObjects--; + } Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; @@ -229,12 +266,12 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { - double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1; - double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1; - double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1; + double rollingAccuracyRatio = rollingMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / rollingMaximumScoringValues.BaseScore : 1; + double accuracyRatio = maximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 1; + double comboRatio = maximumScoringValues.MaxCombo > 0 ? currentScoringValues.MaxCombo / maximumScoringValues.MaxCombo : 1; Accuracy.Value = rollingAccuracyRatio; - TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects); + TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maximumScoringValues.HitObjects); } /// @@ -286,10 +323,10 @@ namespace osu.Game.Rulesets.Scoring out _, out _); - double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1; - double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; + double accuracyRatio = maximumScoringValues.BaseScore > 0 ? extractedBaseScore / maximumScoringValues.BaseScore : 1; + double comboRatio = maximumScoringValues.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / maximumScoringValues.MaxCombo : 1; - return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects); + return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maximumScoringValues.HitObjects); } /// @@ -398,15 +435,10 @@ namespace osu.Game.Rulesets.Scoring lastHitObject = null; if (storeResults) - { - maxAchievableCombo = HighestCombo.Value; - maxBaseScore = baseScore; - maxBasicHitObjects = basicHitObjects; - } + maximumScoringValues = currentScoringValues; - baseScore = 0; - rollingMaxBaseScore = 0; - basicHitObjects = 0; + currentScoringValues = default; + rollingMaximumScoringValues = default; TotalScore.Value = 0; Accuracy.Value = 1; @@ -440,7 +472,10 @@ namespace osu.Game.Rulesets.Scoring if (frame.Header == null) return; - extractFromStatistics(frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _); + extractFromStatistics(frame.Header.Statistics, out double baseScore, out double rollingMaxBaseScore, out _, out _); + currentScoringValues.BaseScore = baseScore; + rollingMaximumScoringValues.BaseScore = rollingMaxBaseScore; + HighestCombo.Value = frame.Header.MaxCombo; scoreResultCounts.Clear(); diff --git a/osu.Game/Scoring/ScoringValues.cs b/osu.Game/Scoring/ScoringValues.cs new file mode 100644 index 0000000000..4b562c20e4 --- /dev/null +++ b/osu.Game/Scoring/ScoringValues.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using MessagePack; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Scoring +{ + [MessagePackObject] + public struct ScoringValues + { + /// + /// The sum of all "basic" scoring values. See: and . + /// + [Key(0)] + public double BaseScore; + + /// + /// The sum of all "bonus" scoring values. See: and . + /// + [Key(1)] + public double BonusScore; + + /// + /// The highest achieved combo. + /// + [Key(2)] + public int MaxCombo; + + /// + /// The count of "basic" s. See: . + /// + [Key(3)] + public int HitObjects; + } +} From d0e3e50ca7c3ca2fa8b6a40ab74805b0e2904c58 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 17:50:18 +0900 Subject: [PATCH 28/37] Extract score statistics directly into ScoringValues --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 104 +++++++++----------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 493da763cb..c108380396 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -288,16 +288,12 @@ namespace osu.Game.Rulesets.Scoring if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - extractFromStatistics(scoreInfo.Statistics, - out double extractedBaseScore, - out double extractedMaxBaseScore, - out int extractedMaxCombo, - out int extractedBasicHitObjects); + extractFromStatistics(scoreInfo.Statistics, out var current, out var max); - double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1; - double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1; + double accuracyRatio = max.BaseScore > 0 ? current.BaseScore / max.BaseScore : 1; + double comboRatio = max.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / max.MaxCombo : 1; - return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects); + return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), max.HitObjects); } /// @@ -317,13 +313,9 @@ namespace osu.Game.Rulesets.Scoring if (!beatmapApplied) throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); - extractFromStatistics(scoreInfo.Statistics, - out double extractedBaseScore, - out _, - out _, - out _); + extractFromStatistics(scoreInfo.Statistics, out var current, out _); - double accuracyRatio = maximumScoringValues.BaseScore > 0 ? extractedBaseScore / maximumScoringValues.BaseScore : 1; + double accuracyRatio = maximumScoringValues.BaseScore > 0 ? current.BaseScore / maximumScoringValues.BaseScore : 1; double comboRatio = maximumScoringValues.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / maximumScoringValues.MaxCombo : 1; return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maximumScoringValues.HitObjects); @@ -353,14 +345,9 @@ namespace osu.Game.Rulesets.Scoring // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3) { - extractFromStatistics(scoreInfo.Statistics, - out double computedBaseScore, - out double computedMaxBaseScore, - out _, - out _); - - if (computedMaxBaseScore > 0) - accuracyRatio = computedBaseScore / computedMaxBaseScore; + extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); + if (maximum.BaseScore > 0) + accuracyRatio = current.BaseScore / current.MaxCombo; } int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum(); @@ -472,10 +459,7 @@ namespace osu.Game.Rulesets.Scoring if (frame.Header == null) return; - extractFromStatistics(frame.Header.Statistics, out double baseScore, out double rollingMaxBaseScore, out _, out _); - currentScoringValues.BaseScore = baseScore; - rollingMaximumScoringValues.BaseScore = rollingMaxBaseScore; - + extractFromStatistics(frame.Header.Statistics, out currentScoringValues, out rollingMaximumScoringValues); HighestCombo.Value = frame.Header.MaxCombo; scoreResultCounts.Clear(); @@ -486,49 +470,59 @@ namespace osu.Game.Rulesets.Scoring OnResetFromReplayFrame?.Invoke(); } - private void extractFromStatistics(IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo, - out int basicHitObjects) + private void extractFromStatistics(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum) { - baseScore = 0; - maxBaseScore = 0; - maxCombo = 0; - basicHitObjects = 0; + current = default; + maximum = default; foreach ((HitResult result, int count) in statistics) { - // Bonus scores are counted separately directly from the statistics dictionary later on. - if (!result.IsScorable() || result.IsBonus()) + if (!result.IsScorable()) continue; - // The maximum result of this judgement if it wasn't a miss. - // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). - HitResult maxResult; - - switch (result) + if (result.IsBonus()) { - case HitResult.LargeTickHit: - case HitResult.LargeTickMiss: - maxResult = HitResult.LargeTickHit; - break; + current.BonusScore += count * Judgement.ToNumericResult(result); + maximum.BonusScore += count * Judgement.ToNumericResult(result); + } + else + { + // The maximum result of this judgement if it wasn't a miss. + // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). + HitResult maxResult; - case HitResult.SmallTickHit: - case HitResult.SmallTickMiss: - maxResult = HitResult.SmallTickHit; - break; + switch (result) + { + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + maxResult = HitResult.LargeTickHit; + break; - default: - maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; - break; + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + maxResult = HitResult.SmallTickHit; + break; + + default: + maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result; + break; + } + + current.BaseScore += count * Judgement.ToNumericResult(result); + maximum.BaseScore += count * Judgement.ToNumericResult(maxResult); } - baseScore += count * Judgement.ToNumericResult(result); - maxBaseScore += count * Judgement.ToNumericResult(maxResult); - if (result.AffectsCombo()) - maxCombo += count; + { + current.MaxCombo += count; + maximum.MaxCombo += count; + } if (result.IsBasic()) - basicHitObjects += count; + { + current.HitObjects += count; + maximum.HitObjects += count; + } } } From a809a19eeca00c59d67b7b3e638526137ea95e0d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 17:51:46 +0900 Subject: [PATCH 29/37] Remove getBonusScore() --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 29 ++++++--------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index c108380396..372fcbb061 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -271,7 +271,7 @@ namespace osu.Game.Rulesets.Scoring double comboRatio = maximumScoringValues.MaxCombo > 0 ? currentScoringValues.MaxCombo / maximumScoringValues.MaxCombo : 1; Accuracy.Value = rollingAccuracyRatio; - TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maximumScoringValues.HitObjects); + TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, currentScoringValues.BonusScore, maximumScoringValues.HitObjects); } /// @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Scoring double accuracyRatio = max.BaseScore > 0 ? current.BaseScore / max.BaseScore : 1; double comboRatio = max.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / max.MaxCombo : 1; - return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), max.HitObjects); + return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, max.HitObjects); } /// @@ -318,7 +318,7 @@ namespace osu.Game.Rulesets.Scoring double accuracyRatio = maximumScoringValues.BaseScore > 0 ? current.BaseScore / maximumScoringValues.BaseScore : 1; double comboRatio = maximumScoringValues.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / maximumScoringValues.MaxCombo : 1; - return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maximumScoringValues.HitObjects); + return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximumScoringValues.HitObjects); } /// @@ -340,19 +340,15 @@ namespace osu.Game.Rulesets.Scoring double accuracyRatio = scoreInfo.Accuracy; double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; + extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); + // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. - if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3) - { - extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); - if (maximum.BaseScore > 0) - accuracyRatio = current.BaseScore / current.MaxCombo; - } + if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0) + accuracyRatio = current.BaseScore / current.MaxCombo; - int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum(); - - return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects); + return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); } /// @@ -382,15 +378,6 @@ namespace osu.Game.Rulesets.Scoring } } - /// - /// Calculates the total bonus score from score statistics. - /// - /// The score statistics. - /// The total bonus score. - private double getBonusScore(IReadOnlyDictionary statistics) - => statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE - + statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; - private ScoreRank rankFrom(double acc) { if (acc == 1) From 2289812801ffc632dd318e626688b68b18613428 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 17:57:12 +0900 Subject: [PATCH 30/37] Add method to compute score from ScoringValues --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 372fcbb061..27c2712641 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -351,6 +351,20 @@ namespace osu.Game.Rulesets.Scoring return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); } + /// + /// Computes the total score from scoring values. + /// + /// The to represent the score as. + /// The current scoring values. + /// The maximum scoring values. + /// The total score computed from the given scoring values. + public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) + { + double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1; + double comboRatio = maximum.MaxCombo > 0 ? current.MaxCombo / maximum.MaxCombo : 1; + return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); + } + /// /// Computes the total score from individual scoring components. /// From 6ccdb618531dd10a906bcc60649a1545a0cacb16 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 18:01:02 +0900 Subject: [PATCH 31/37] Use new ComputeScore() overload in more cases --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 22 +++++++-------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 27c2712641..b286839fa7 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -266,12 +266,8 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { - double rollingAccuracyRatio = rollingMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / rollingMaximumScoringValues.BaseScore : 1; - double accuracyRatio = maximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 1; - double comboRatio = maximumScoringValues.MaxCombo > 0 ? currentScoringValues.MaxCombo / maximumScoringValues.MaxCombo : 1; - - Accuracy.Value = rollingAccuracyRatio; - TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, currentScoringValues.BonusScore, maximumScoringValues.HitObjects); + Accuracy.Value = rollingMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / rollingMaximumScoringValues.BaseScore : 1; + TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues); } /// @@ -288,12 +284,10 @@ namespace osu.Game.Rulesets.Scoring if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - extractFromStatistics(scoreInfo.Statistics, out var current, out var max); + extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); + current.MaxCombo = scoreInfo.MaxCombo; - double accuracyRatio = max.BaseScore > 0 ? current.BaseScore / max.BaseScore : 1; - double comboRatio = max.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / max.MaxCombo : 1; - - return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, max.HitObjects); + return ComputeScore(mode, current, maximum); } /// @@ -314,11 +308,9 @@ namespace osu.Game.Rulesets.Scoring throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); extractFromStatistics(scoreInfo.Statistics, out var current, out _); + current.MaxCombo = scoreInfo.MaxCombo; - double accuracyRatio = maximumScoringValues.BaseScore > 0 ? current.BaseScore / maximumScoringValues.BaseScore : 1; - double comboRatio = maximumScoringValues.MaxCombo > 0 ? (double)scoreInfo.MaxCombo / maximumScoringValues.MaxCombo : 1; - - return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximumScoringValues.HitObjects); + return ComputeScore(mode, current, maximumScoringValues); } /// From 20988be6bbad41cc663a8a216096b4a9c6bc5799 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 18:01:11 +0900 Subject: [PATCH 32/37] Fix incorrect value --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b286839fa7..18b978b413 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Scoring // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together. if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0) - accuracyRatio = current.BaseScore / current.MaxCombo; + accuracyRatio = current.BaseScore / maximum.BaseScore; return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); } From 44ca350822afee38e7f2c8f464a8b2d608871de2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 18:17:25 +0900 Subject: [PATCH 33/37] Reset minimal scoring values from frames --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 18b978b413..6351d15649 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -452,7 +452,12 @@ namespace osu.Game.Rulesets.Scoring if (frame.Header == null) return; - extractFromStatistics(frame.Header.Statistics, out currentScoringValues, out rollingMaximumScoringValues); + extractFromStatistics(frame.Header.Statistics, out var current, out var maximum); + currentScoringValues.BaseScore = current.BaseScore; + currentScoringValues.MaxCombo = frame.Header.MaxCombo; + rollingMaximumScoringValues.BaseScore = maximum.BaseScore; + rollingMaximumScoringValues.MaxCombo = maximum.MaxCombo; + HighestCombo.Value = frame.Header.MaxCombo; scoreResultCounts.Clear(); From d6d56ee22dfed6ab20f195efb7d53d48a4b9de35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 18:17:40 +0900 Subject: [PATCH 34/37] Fix unintentional truncation --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 6351d15649..afeef9341a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -353,7 +353,7 @@ namespace osu.Game.Rulesets.Scoring public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) { double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1; - double comboRatio = maximum.MaxCombo > 0 ? current.MaxCombo / maximum.MaxCombo : 1; + double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1; return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects); } From 28d8799e119318b860c1d551ba79b66645a8682c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 18:39:29 +0900 Subject: [PATCH 35/37] Add overloads to + document + expose ExtractScoringValues --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 89 ++++++++++++++++++--- 1 file changed, 77 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index afeef9341a..96cf4bcc24 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Extensions; +using osu.Game.Online.Spectator; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -89,13 +90,16 @@ namespace osu.Game.Rulesets.Scoring private readonly double comboPortion; /// - /// Maximum achievable scoring values. + /// Scoring values for a perfect play. /// private ScoringValues maximumScoringValues; /// /// Maximum achievable scoring values up to the current point in time. /// + /// + /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session. + /// private ScoringValues rollingMaximumScoringValues; /// @@ -284,7 +288,7 @@ namespace osu.Game.Rulesets.Scoring if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); - extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); + extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); current.MaxCombo = scoreInfo.MaxCombo; return ComputeScore(mode, current, maximum); @@ -307,7 +311,7 @@ namespace osu.Game.Rulesets.Scoring if (!beatmapApplied) throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}."); - extractFromStatistics(scoreInfo.Statistics, out var current, out _); + extractScoringValues(scoreInfo.Statistics, out var current, out _); current.MaxCombo = scoreInfo.MaxCombo; return ComputeScore(mode, current, maximumScoringValues); @@ -332,7 +336,7 @@ namespace osu.Game.Rulesets.Scoring double accuracyRatio = scoreInfo.Accuracy; double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1; - extractFromStatistics(scoreInfo.Statistics, out var current, out var maximum); + extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); // For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score. // To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score. @@ -452,7 +456,7 @@ namespace osu.Game.Rulesets.Scoring if (frame.Header == null) return; - extractFromStatistics(frame.Header.Statistics, out var current, out var maximum); + extractScoringValues(frame.Header.Statistics, out var current, out var maximum); currentScoringValues.BaseScore = current.BaseScore; currentScoringValues.MaxCombo = frame.Header.MaxCombo; rollingMaximumScoringValues.BaseScore = maximum.BaseScore; @@ -468,7 +472,72 @@ namespace osu.Game.Rulesets.Scoring OnResetFromReplayFrame?.Invoke(); } - private void extractFromStatistics(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum) + #region ScoringValue extraction + + /// + /// Applies a best-effort extraction of hit statistics into . + /// + /// + /// This method is useful in a variety of situations, with a few drawbacks that need to be considered: + /// + /// The maximum will always be 0. + /// The current and maximum will always be the same value. + /// + /// Consumers are expected to more accurately fill in the above values through external means. + /// + /// Ensure to fill in the maximum for use in + /// . + /// + /// + /// The score to extract scoring values from. + /// The "current" scoring values, representing the hit statistics as they appear. + /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time. + public void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum) + { + extractScoringValues(scoreInfo.Statistics, out current, out maximum); + current.MaxCombo = scoreInfo.MaxCombo; + } + + /// + /// Applies a best-effort extraction of hit statistics into . + /// + /// + /// This method is useful in a variety of situations, with a few drawbacks that need to be considered: + /// + /// The maximum will always be 0. + /// The current and maximum will always be the same value. + /// + /// Consumers are expected to more accurately fill in the above values through external means. + /// + /// Ensure to fill in the maximum for use in + /// . + /// + /// + /// The replay frame header to extract scoring values from. + /// The "current" scoring values, representing the hit statistics as they appear. + /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time. + public void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum) + { + extractScoringValues(header.Statistics, out current, out maximum); + current.MaxCombo = header.MaxCombo; + } + + /// + /// Applies a best-effort extraction of hit statistics into . + /// + /// + /// This method is useful in a variety of situations, with a few drawbacks that need to be considered: + /// + /// The current will always be 0. + /// The maximum will always be 0. + /// The current and maximum will always be the same value. + /// + /// Consumers are expected to more accurately fill in the above values (especially the current ) via external means (e.g. ). + /// + /// The hit statistics to extract scoring values from. + /// The "current" scoring values, representing the hit statistics as they appear. + /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time. + private void extractScoringValues(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum) { current = default; maximum = default; @@ -479,10 +548,7 @@ namespace osu.Game.Rulesets.Scoring continue; if (result.IsBonus()) - { current.BonusScore += count * Judgement.ToNumericResult(result); - maximum.BonusScore += count * Judgement.ToNumericResult(result); - } else { // The maximum result of this judgement if it wasn't a miss. @@ -511,10 +577,7 @@ namespace osu.Game.Rulesets.Scoring } if (result.AffectsCombo()) - { - current.MaxCombo += count; maximum.MaxCombo += count; - } if (result.IsBasic()) { @@ -524,6 +587,8 @@ namespace osu.Game.Rulesets.Scoring } } + #endregion + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 93240073a645642f540fa9aab46e2fa7ce52490a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 19:34:51 +0900 Subject: [PATCH 36/37] Rename field + rewrite xmldoc a bit --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 96cf4bcc24..4463941ec2 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Scoring private ScoringValues maximumScoringValues; /// - /// Maximum achievable scoring values up to the current point in time. + /// Scoring values for the current play assuming all perfect hits. /// /// /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session. /// - private ScoringValues rollingMaximumScoringValues; + private ScoringValues currentMaximumScoringValues; /// /// Scoring values for the current play. @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Scoring { // The inverse of non-scorable (ignore) judgements may be bonus judgements. if (result.Judgement.MaxResult.IsBonus()) - rollingMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; return; } @@ -184,25 +184,25 @@ namespace osu.Game.Rulesets.Scoring // Update maximum combo. currentScoringValues.MaxCombo = HighestCombo.Value; - rollingMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; + currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; // Update base/bonus score. if (result.Type.IsBonus()) { currentScoringValues.BonusScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - rollingMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult; } else { currentScoringValues.BaseScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - rollingMaximumScoringValues.BaseScore += result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BaseScore += result.Judgement.MaxNumericResult; } // Update hitobject count. if (result.Type.IsBasic()) { currentScoringValues.HitObjects++; - rollingMaximumScoringValues.HitObjects++; + currentMaximumScoringValues.HitObjects++; } hitEvents.Add(CreateHitEvent(result)); @@ -233,32 +233,32 @@ namespace osu.Game.Rulesets.Scoring { // The inverse of non-scorable (ignore) judgements may be bonus judgements. if (result.Judgement.MaxResult.IsBonus()) - rollingMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; return; } // Update maximum combo. currentScoringValues.MaxCombo = HighestCombo.Value; - rollingMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; + currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.AffectsCombo() ? 1 : 0; // Update base/bonus score. if (result.Type.IsBonus()) { currentScoringValues.BonusScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - rollingMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult; } else { currentScoringValues.BaseScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - rollingMaximumScoringValues.BaseScore -= result.Judgement.MaxNumericResult; + currentMaximumScoringValues.BaseScore -= result.Judgement.MaxNumericResult; } // Update hitobject count. if (result.Type.IsBasic()) { currentScoringValues.HitObjects--; - rollingMaximumScoringValues.HitObjects--; + currentMaximumScoringValues.HitObjects--; } Debug.Assert(hitEvents.Count > 0); @@ -270,7 +270,7 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { - Accuracy.Value = rollingMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / rollingMaximumScoringValues.BaseScore : 1; + Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues); } @@ -422,7 +422,7 @@ namespace osu.Game.Rulesets.Scoring maximumScoringValues = currentScoringValues; currentScoringValues = default; - rollingMaximumScoringValues = default; + currentMaximumScoringValues = default; TotalScore.Value = 0; Accuracy.Value = 1; @@ -459,8 +459,8 @@ namespace osu.Game.Rulesets.Scoring extractScoringValues(frame.Header.Statistics, out var current, out var maximum); currentScoringValues.BaseScore = current.BaseScore; currentScoringValues.MaxCombo = frame.Header.MaxCombo; - rollingMaximumScoringValues.BaseScore = maximum.BaseScore; - rollingMaximumScoringValues.MaxCombo = maximum.MaxCombo; + currentMaximumScoringValues.BaseScore = maximum.BaseScore; + currentMaximumScoringValues.MaxCombo = maximum.MaxCombo; HighestCombo.Value = frame.Header.MaxCombo; From 1e0ee1b2147ff532e7f3cea26e0c2708c4077fdc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 31 May 2022 19:49:37 +0900 Subject: [PATCH 37/37] Expose MaximumScoringValues for user consumption --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4463941ec2..8e69ffa0a0 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Scoring values for a perfect play. /// - private ScoringValues maximumScoringValues; + public ScoringValues MaximumScoringValues { get; private set; } /// /// Scoring values for the current play assuming all perfect hits. @@ -271,7 +271,7 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; - TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues); + TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, MaximumScoringValues); } /// @@ -314,7 +314,7 @@ namespace osu.Game.Rulesets.Scoring extractScoringValues(scoreInfo.Statistics, out var current, out _); current.MaxCombo = scoreInfo.MaxCombo; - return ComputeScore(mode, current, maximumScoringValues); + return ComputeScore(mode, current, MaximumScoringValues); } /// @@ -419,7 +419,7 @@ namespace osu.Game.Rulesets.Scoring lastHitObject = null; if (storeResults) - maximumScoringValues = currentScoringValues; + MaximumScoringValues = currentScoringValues; currentScoringValues = default; currentMaximumScoringValues = default;