From a294f328fb4f1471273b5ebe2e49b8e7e2003f21 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 21 Mar 2021 06:30:17 +0100 Subject: [PATCH 01/92] Add linear circular arc test --- .../TestSceneSliderPlacementBlueprint.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 67a2e5a47c..f9b6fb924e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -276,6 +276,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Linear); } + [Test] + public void TestPlacePerfectCurveSegmentAlmostLinearly() + { + addMovementStep(new Vector2(0)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(61.382935f, 6.423767f)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(217.69522f, 22.84021f)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + // Will be > 10000 if not falling back to Bezier path calc. + assertLength(218.8901f); + } + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addClickStep(MouseButton button) From fcd1f4930f9118445a0976a785db75282bf9763b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Sun, 21 Mar 2021 06:34:55 +0100 Subject: [PATCH 02/92] Fix freeze due to large circular arc radius Seems to stem from the osu!framework's PathApproximator not catching a few edge cases wherein the radius approaches infinity. --- osu.Game/Rulesets/Objects/SliderPath.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 3083fcfccb..94815fe0ea 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -219,8 +219,10 @@ namespace osu.Game.Rulesets.Objects List subpath = PathApproximator.ApproximateCircularArc(subControlPoints); - // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. - if (subpath.Count == 0) + // If for some reason a circular arc could not be fit to the 3 given points, or the + // resulting radius is too large (e.g. points arranged almost linearly), fall back + // to a numerically stable bezier approximation. + if (subpath.Count == 0 || subpath.Count > 400) break; return subpath; From bee2f55d007bad3e83fd2d010f709ff720242784 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:54:33 +0100 Subject: [PATCH 03/92] Undo subpath limiting --- osu.Game/Rulesets/Objects/SliderPath.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 94815fe0ea..3083fcfccb 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -219,10 +219,8 @@ namespace osu.Game.Rulesets.Objects List subpath = PathApproximator.ApproximateCircularArc(subControlPoints); - // If for some reason a circular arc could not be fit to the 3 given points, or the - // resulting radius is too large (e.g. points arranged almost linearly), fall back - // to a numerically stable bezier approximation. - if (subpath.Count == 0 || subpath.Count > 400) + // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. + if (subpath.Count == 0) break; return subpath; From 7a2cb526e4bd9e04f5e0720336cf278551d4efac Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:55:30 +0100 Subject: [PATCH 04/92] Add PointsInSegment method --- osu.Game/Rulesets/Objects/SliderPath.cs | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 3083fcfccb..e76699ac04 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -156,6 +156,38 @@ namespace osu.Game.Rulesets.Objects return interpolateVertices(indexOfDistance(d), d); } + /// + /// Returns the control points belonging to the same segment as the one given. + /// The first point has a PathType which all other points inherit. + /// + /// One of the control points in the segment. + /// + public List PointsInSegment(PathControlPoint controlPoint) + { + bool found = false; + List pointsInCurrentSegment = new List(); + foreach (PathControlPoint point in ControlPoints) + { + if (point.Type.Value is PathType) + { + if (!found) + pointsInCurrentSegment.Clear(); + else + { + pointsInCurrentSegment.Add(point); + break; + } + } + + pointsInCurrentSegment.Add(point); + + if (point == controlPoint) + found = true; + } + + return pointsInCurrentSegment; + } + private void invalidate() { pathCache.Invalidate(); From c82218627f914ca51875422de48811dacb136c14 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:57:57 +0100 Subject: [PATCH 05/92] Add path type update logic Only attempts to change points to bezier if points in the slider are modified. --- osu.Game/Rulesets/Objects/SliderPath.cs | 53 +++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index e76699ac04..42f1a33dfc 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -54,13 +54,19 @@ namespace osu.Game.Rulesets.Objects { case NotifyCollectionChangedAction.Add: foreach (var c in args.NewItems.Cast()) + { c.Changed += invalidate; + c.Changed += updatePathTypes; + } break; case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: foreach (var c in args.OldItems.Cast()) + { c.Changed -= invalidate; + c.Changed -= updatePathTypes; + } break; } @@ -188,6 +194,53 @@ namespace osu.Game.Rulesets.Objects return pointsInCurrentSegment; } + private void updatePathTypes() + { + // Updates each segment of the slider once + foreach (PathControlPoint controlPoint in ControlPoints.Where(p => p.Type.Value is PathType)) + updatePathType(controlPoint); + } + + private void updatePathType(PathControlPoint controlPoint) + { + List pointsInSegment = PointsInSegment(controlPoint); + PathType? pathType = pointsInSegment[0].Type.Value; + + // An almost linear arrangement of points results in radius approaching infinity, + // we should prevent that to avoid memory exhaustion when drawing / approximating + if (pathType == PathType.PerfectCurve) + { + Vector2[] points = pointsInSegment.Select(point => point.Position.Value).ToArray(); + if (points.Length < 3) + return; + + Vector2 a = points[0]; + Vector2 b = points[1]; + Vector2 c = points[2]; + + float maxLength = points.Max(p => p.Length); + + Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); + Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); + Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); + + float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); + + float acSq = (a - c).LengthSquared; + float abSq = (a - b).LengthSquared; + float bcSq = (b - c).LengthSquared; + + // Exterior = curve wraps around the long way between end-points + // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, + // where the latter is much faster, hence differing thresholds + bool exterior = abSq > acSq || bcSq > acSq; + float threshold = exterior ? 0.05f : 0.001f; + + if (Math.Abs(det) < threshold) + pointsInSegment[0].Type.Value = PathType.Bezier; + } + } + private void invalidate() { pathCache.Invalidate(); From 067178e53779be114ffd067a28144bb6dee94dd4 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:59:06 +0100 Subject: [PATCH 06/92] Maintain path type when dragging/placing --- .../Sliders/Components/PathControlPointPiece.cs | 10 ++++++++++ .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 3 +++ 2 files changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 1390675a1a..42a7d246ea 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public readonly BindableBool IsSelected = new BindableBool(); public readonly PathControlPoint ControlPoint; + public readonly List PointsInSegment; private readonly Slider slider; private readonly Container marker; @@ -53,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; + PointsInSegment = slider.Path.PointsInSegment(controlPoint); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); @@ -150,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnClick(ClickEvent e) => RequestSelection != null; private Vector2 dragStartPosition; + private PathType? dragPathType; protected override bool OnDragStart(DragStartEvent e) { @@ -159,6 +163,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (e.Button == MouseButton.Left) { dragStartPosition = ControlPoint.Position.Value; + dragPathType = PointsInSegment[0].Type.Value; + changeHandler?.BeginChange(); return true; } @@ -184,6 +190,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } else ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); + + // Maintain the path type in case it got defaulted to bezier at some point during the drag. + if (PointsInSegment[0].Type.Value != dragPathType) + PointsInSegment[0].Type.Value = dragPathType; } protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index b71e1914f7..16e2a52279 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -142,6 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.Update(); updateSlider(); + + // Maintain the path type in case it got defaulted to bezier at some point during the drag. + updatePathType(); } private void updatePathType() From 3bddc4a75d2f312541aad38e6408d956c32f9d81 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:59:45 +0100 Subject: [PATCH 07/92] Add path type test --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index f9b6fb924e..4f716a31b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -290,6 +290,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); + assertControlPointType(0, PathType.Bezier); // Will be > 10000 if not falling back to Bezier path calc. assertLength(218.8901f); } From 15af57de95ec260afef3a59bedc736202c06f297 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 15:59:59 +0100 Subject: [PATCH 08/92] Add path type recovery test --- .../TestSceneSliderPlacementBlueprint.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 4f716a31b7..db7b3aa973 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -295,6 +295,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertLength(218.8901f); } + [Test] + public void TestPlacePerfectCurveSegmentAlmostLinearlyRecovery() + { + addMovementStep(new Vector2(0)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(61.382935f, 6.423767f)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(217.69522f, 22.84021f)); // Should convert to bezier + addMovementStep(new Vector2(210.0f, 30.0f)); // Should convert back to perfect + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.PerfectCurve); + assertLength(212.2276f); + } + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addClickStep(MouseButton button) From 323b875cea686e080c3b58fc04de91e73d24b437 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:32:40 +0100 Subject: [PATCH 09/92] Fix newlines/spaces --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 4 ++-- osu.Game/Rulesets/Objects/SliderPath.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index db7b3aa973..e7ea12e538 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -304,8 +304,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(new Vector2(61.382935f, 6.423767f)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(217.69522f, 22.84021f)); // Should convert to bezier - addMovementStep(new Vector2(210.0f, 30.0f)); // Should convert back to perfect + addMovementStep(new Vector2(217.69522f, 22.84021f)); // Should convert to bezier + addMovementStep(new Vector2(210.0f, 30.0f)); // Should convert back to perfect addClickStep(MouseButton.Right); assertPlaced(true); diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 42f1a33dfc..639cd4c72e 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -58,6 +58,7 @@ namespace osu.Game.Rulesets.Objects c.Changed += invalidate; c.Changed += updatePathTypes; } + break; case NotifyCollectionChangedAction.Reset: @@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Objects c.Changed -= invalidate; c.Changed -= updatePathTypes; } + break; } From a7076c329c66752955ee7501b09a1a17b7554b30 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 17:32:55 +0100 Subject: [PATCH 10/92] Fix null checks --- osu.Game/Rulesets/Objects/SliderPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 639cd4c72e..47cc2c6044 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Objects List pointsInCurrentSegment = new List(); foreach (PathControlPoint point in ControlPoints) { - if (point.Type.Value is PathType) + if (point.Type.Value != null) { if (!found) pointsInCurrentSegment.Clear(); @@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Objects private void updatePathTypes() { // Updates each segment of the slider once - foreach (PathControlPoint controlPoint in ControlPoints.Where(p => p.Type.Value is PathType)) + foreach (PathControlPoint controlPoint in ControlPoints.Where(p => p.Type.Value != null)) updatePathType(controlPoint); } From 6911a1b41546c04252b4d9e92bcbbf511befbf2a Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:03:55 +0100 Subject: [PATCH 11/92] Fix missing newline --- osu.Game/Rulesets/Objects/SliderPath.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 47cc2c6044..d8fbb46b76 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -174,6 +174,7 @@ namespace osu.Game.Rulesets.Objects { bool found = false; List pointsInCurrentSegment = new List(); + foreach (PathControlPoint point in ControlPoints) { if (point.Type.Value != null) @@ -241,6 +242,12 @@ namespace osu.Game.Rulesets.Objects if (Math.Abs(det) < threshold) pointsInSegment[0].Type.Value = PathType.Bezier; } + // where the latter is much faster, hence differing thresholds + bool exterior = abSq > acSq || bcSq > acSq; + float threshold = exterior ? 0.05f : 0.001f; + + if (Math.Abs(det) < threshold) + pointsInSegment[0].Type.Value = PathType.Bezier; } private void invalidate() From 80e7c3aba7e804ae1f1696219ee8c9a48871abb0 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:09:28 +0100 Subject: [PATCH 12/92] Invert if statement --- osu.Game/Rulesets/Objects/SliderPath.cs | 42 +++++++++++-------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index d8fbb46b76..767f7909d5 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -211,37 +211,31 @@ namespace osu.Game.Rulesets.Objects // An almost linear arrangement of points results in radius approaching infinity, // we should prevent that to avoid memory exhaustion when drawing / approximating - if (pathType == PathType.PerfectCurve) - { - Vector2[] points = pointsInSegment.Select(point => point.Position.Value).ToArray(); - if (points.Length < 3) - return; + if (pathType != PathType.PerfectCurve) + return; - Vector2 a = points[0]; - Vector2 b = points[1]; - Vector2 c = points[2]; + Vector2[] points = pointsInSegment.Select(point => point.Position.Value).ToArray(); + if (points.Length < 3) + return; - float maxLength = points.Max(p => p.Length); + Vector2 a = points[0]; + Vector2 b = points[1]; + Vector2 c = points[2]; - Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); - Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); - Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); + float maxLength = points.Max(p => p.Length); - float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); + Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); + Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); + Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); - float acSq = (a - c).LengthSquared; - float abSq = (a - b).LengthSquared; - float bcSq = (b - c).LengthSquared; + float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); - // Exterior = curve wraps around the long way between end-points - // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, - // where the latter is much faster, hence differing thresholds - bool exterior = abSq > acSq || bcSq > acSq; - float threshold = exterior ? 0.05f : 0.001f; + float acSq = (a - c).LengthSquared; + float abSq = (a - b).LengthSquared; + float bcSq = (b - c).LengthSquared; - if (Math.Abs(det) < threshold) - pointsInSegment[0].Type.Value = PathType.Bezier; - } + // Exterior = curve wraps around the long way between end-points + // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, // where the latter is much faster, hence differing thresholds bool exterior = abSq > acSq || bcSq > acSq; float threshold = exterior ? 0.05f : 0.001f; From 92f713a30e57d86b9c6408004be911f8c40755ca Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:10:56 +0100 Subject: [PATCH 13/92] Improve fallback conditions It's possible to create a `PerfectCurve` type path with more than 3 points currently, so this accounts for that. --- osu.Game/Rulesets/Objects/SliderPath.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 767f7909d5..23511be5e4 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -211,12 +211,10 @@ namespace osu.Game.Rulesets.Objects // An almost linear arrangement of points results in radius approaching infinity, // we should prevent that to avoid memory exhaustion when drawing / approximating - if (pathType != PathType.PerfectCurve) + if (pathType != PathType.PerfectCurve || pointsInSegment.Count != 3) return; Vector2[] points = pointsInSegment.Select(point => point.Position.Value).ToArray(); - if (points.Length < 3) - return; Vector2 a = points[0]; Vector2 b = points[1]; From b11fd7972aafea70905a1f0f73addffe82442d91 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:41:48 +0100 Subject: [PATCH 14/92] Separate condition logic from math logic --- osu.Game/Rulesets/Objects/SliderPath.cs | 27 ++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 23511be5e4..ae08660404 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -199,23 +199,19 @@ namespace osu.Game.Rulesets.Objects private void updatePathTypes() { - // Updates each segment of the slider once - foreach (PathControlPoint controlPoint in ControlPoints.Where(p => p.Type.Value != null)) - updatePathType(controlPoint); + foreach (PathControlPoint segmentStartPoint in ControlPoints.Where(p => p.Type.Value != null)) + { + if (segmentStartPoint.Type.Value != PathType.PerfectCurve) + continue; + + Vector2[] points = PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); + if (points.Length == 3 && !validCircularArcSegment(points)) + segmentStartPoint.Type.Value = PathType.Bezier; + } } - private void updatePathType(PathControlPoint controlPoint) + private bool validCircularArcSegment(IReadOnlyList points) { - List pointsInSegment = PointsInSegment(controlPoint); - PathType? pathType = pointsInSegment[0].Type.Value; - - // An almost linear arrangement of points results in radius approaching infinity, - // we should prevent that to avoid memory exhaustion when drawing / approximating - if (pathType != PathType.PerfectCurve || pointsInSegment.Count != 3) - return; - - Vector2[] points = pointsInSegment.Select(point => point.Position.Value).ToArray(); - Vector2 a = points[0]; Vector2 b = points[1]; Vector2 c = points[2]; @@ -238,8 +234,7 @@ namespace osu.Game.Rulesets.Objects bool exterior = abSq > acSq || bcSq > acSq; float threshold = exterior ? 0.05f : 0.001f; - if (Math.Abs(det) < threshold) - pointsInSegment[0].Type.Value = PathType.Bezier; + return Math.Abs(det) < threshold; } private void invalidate() From 3fa5852e00cbb6d43e0383e5044dcd487ab43b2a Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:42:27 +0100 Subject: [PATCH 15/92] Add method documentation --- osu.Game/Rulesets/Objects/SliderPath.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index ae08660404..b048bf2a84 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -197,6 +197,9 @@ namespace osu.Game.Rulesets.Objects return pointsInCurrentSegment; } + /// + /// Handles correction of invalid path types. + /// private void updatePathTypes() { foreach (PathControlPoint segmentStartPoint in ControlPoints.Where(p => p.Type.Value != null)) @@ -210,6 +213,13 @@ namespace osu.Game.Rulesets.Objects } } + /// + /// Returns whether the given points are arranged in a valid way. Invalid if points + /// are almost entirely linear - as this causes the radius to approach infinity, + /// which would exhaust memory when drawing / approximating. + /// + /// The three points that make up this circular arc segment. + /// private bool validCircularArcSegment(IReadOnlyList points) { Vector2 a = points[0]; From e922e67c9847b8dfea62bed7c85d673acd6278bd Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 22 Mar 2021 19:48:21 +0100 Subject: [PATCH 16/92] Fix inverted return statement Forgot to run tests, all passing now. --- osu.Game/Rulesets/Objects/SliderPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index b048bf2a84..c7931b440b 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Objects bool exterior = abSq > acSq || bcSq > acSq; float threshold = exterior ? 0.05f : 0.001f; - return Math.Abs(det) < threshold; + return Math.Abs(det) >= threshold; } private void invalidate() From 5ee280f941b5738adf98211646b8f12cc3719702 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 02:56:32 +0100 Subject: [PATCH 17/92] Update PointsInSegment when adding/removing points There was a bug where if you created a slider, moved the last point, and then added a point such that it became a PerfectCurve, it would fail to recover after becoming a Bezier. This fixes that. --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 42a7d246ea..f34403b377 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public class PathControlPointPiece : BlueprintPiece, IHasTooltip { public Action RequestSelection; + public List PointsInSegment; public readonly BindableBool IsSelected = new BindableBool(); public readonly PathControlPoint ControlPoint; - public readonly List PointsInSegment; private readonly Slider slider; private readonly Container marker; @@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; - PointsInSegment = slider.Path.PointsInSegment(controlPoint); + slider.Path.ControlPoints.BindCollectionChanged((_, args) => + { + PointsInSegment = slider.Path.PointsInSegment(controlPoint); + }, runOnceImmediately: true); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); From 0bcd38e6613fff3009a7e59f55c422751cf9e6e5 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 02:57:47 +0100 Subject: [PATCH 18/92] Simplify path type maintenance when dragging --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index f34403b377..4459308ea5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -195,8 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); // Maintain the path type in case it got defaulted to bezier at some point during the drag. - if (PointsInSegment[0].Type.Value != dragPathType) - PointsInSegment[0].Type.Value = dragPathType; + PointsInSegment[0].Type.Value = dragPathType; } protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); From 4ae3eaaac6265b54c649fc64789c84ebd87f4400 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 03:02:19 +0100 Subject: [PATCH 19/92] Move path type correction This is better because `PathControlPointVisualizer` is local to the editor, meaning there is no chance that this could affect gameplay. --- .../Components/PathControlPointVisualiser.cs | 54 +++++++++++++++++ osu.Game/Rulesets/Objects/PathControlPoint.cs | 2 +- osu.Game/Rulesets/Objects/SliderPath.cs | 58 ------------------- 3 files changed, 55 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ce5dc4855e..0c9163ae41 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -91,6 +91,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components })); Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); + + point.Changed += updatePathTypes; } break; @@ -100,6 +102,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Pieces.RemoveAll(p => p.ControlPoint == point); Connections.RemoveAll(c => c.ControlPoint == point); + + point.Changed -= updatePathTypes; } // If removing before the end of the path, @@ -142,6 +146,56 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + /// + /// Handles correction of invalid path types. + /// + private void updatePathTypes() + { + foreach (PathControlPoint segmentStartPoint in slider.Path.ControlPoints.Where(p => p.Type.Value != null)) + { + if (segmentStartPoint.Type.Value != PathType.PerfectCurve) + continue; + + Vector2[] points = slider.Path.PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); + if (points.Length == 3 && !validCircularArcSegment(points)) + segmentStartPoint.Type.Value = PathType.Bezier; + } + } + + /// + /// Returns whether the given points are arranged in a valid way. Invalid if points + /// are almost entirely linear - as this causes the radius to approach infinity, + /// which would exhaust memory when drawing / approximating. + /// + /// The three points that make up this circular arc segment. + /// + private bool validCircularArcSegment(IReadOnlyList points) + { + Vector2 a = points[0]; + Vector2 b = points[1]; + Vector2 c = points[2]; + + float maxLength = points.Max(p => p.Length); + + Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); + Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); + Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); + + float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); + + float acSq = (a - c).LengthSquared; + float abSq = (a - b).LengthSquared; + float bcSq = (b - c).LengthSquared; + + // Exterior = curve wraps around the long way between end-points + // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, + // where the latter is much faster, hence differing thresholds + bool exterior = abSq > acSq || bcSq > acSq; + float threshold = exterior ? 0.05f : 0.001f; + + return Math.Abs(det) >= threshold; + } + private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index f11917f4f4..2e4100ee0b 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects /// /// Invoked when any property of this is changed. /// - internal event Action Changed; + public event Action Changed; /// /// Creates a new . diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index c7931b440b..61f5f94142 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -54,21 +54,13 @@ namespace osu.Game.Rulesets.Objects { case NotifyCollectionChangedAction.Add: foreach (var c in args.NewItems.Cast()) - { c.Changed += invalidate; - c.Changed += updatePathTypes; - } - break; case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: foreach (var c in args.OldItems.Cast()) - { c.Changed -= invalidate; - c.Changed -= updatePathTypes; - } - break; } @@ -197,56 +189,6 @@ namespace osu.Game.Rulesets.Objects return pointsInCurrentSegment; } - /// - /// Handles correction of invalid path types. - /// - private void updatePathTypes() - { - foreach (PathControlPoint segmentStartPoint in ControlPoints.Where(p => p.Type.Value != null)) - { - if (segmentStartPoint.Type.Value != PathType.PerfectCurve) - continue; - - Vector2[] points = PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); - if (points.Length == 3 && !validCircularArcSegment(points)) - segmentStartPoint.Type.Value = PathType.Bezier; - } - } - - /// - /// Returns whether the given points are arranged in a valid way. Invalid if points - /// are almost entirely linear - as this causes the radius to approach infinity, - /// which would exhaust memory when drawing / approximating. - /// - /// The three points that make up this circular arc segment. - /// - private bool validCircularArcSegment(IReadOnlyList points) - { - Vector2 a = points[0]; - Vector2 b = points[1]; - Vector2 c = points[2]; - - float maxLength = points.Max(p => p.Length); - - Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); - Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); - Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); - - float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); - - float acSq = (a - c).LengthSquared; - float abSq = (a - b).LengthSquared; - float bcSq = (b - c).LengthSquared; - - // Exterior = curve wraps around the long way between end-points - // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, - // where the latter is much faster, hence differing thresholds - bool exterior = abSq > acSq || bcSq > acSq; - float threshold = exterior ? 0.05f : 0.001f; - - return Math.Abs(det) >= threshold; - } - private void invalidate() { pathCache.Invalidate(); From 7bae4ff43d1d90c30dd874e54183e60934dd7fd3 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:06:04 +0100 Subject: [PATCH 20/92] Add control point dragging tests --- .../TestSceneSliderControlPointPiece.cs | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs new file mode 100644 index 0000000000..c4f103e40c --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene + { + private Slider slider; + private DrawableSlider drawableObject; + private TestSliderBlueprint blueprint; + + [SetUp] + public void Setup() => Schedule(() => + { + Clear(); + + slider = new Slider + { + Position = new Vector2(256, 192), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150)) + }) + }; + + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); + + Add(drawableObject = new DrawableSlider(slider)); + AddBlueprint(blueprint = new TestSliderBlueprint(drawableObject)); + }); + + [Test] + public void TestDragControlPoint() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(150, 50)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 50)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestDragControlPointAlmostLinearly() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(150, 0.01f)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 0.01f)); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestDragControlPointAlmostLinearlyExterior() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(400, 0.01f)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(400, 0.01f)); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestDragControlPointPathRecovery() + { + moveMouseToControlPoint(1); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(150, 0.01f)); + assertControlPointType(0, PathType.Bezier); + + addMovementStep(new Vector2(150, 50)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 50)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestDragControlPointPathRecoveryOtherSegment() + { + moveMouseToControlPoint(4); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(350, 0)); + assertControlPointType(2, PathType.Bezier); + + addMovementStep(new Vector2(150, 150)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(1, new Vector2(150, 150)); + assertControlPointType(2, PathType.PerfectCurve); + } + + private void addMovementStep(Vector2 relativePosition) + { + AddStep($"move mouse to {relativePosition}", () => + { + Vector2 position = slider.Position + relativePosition; + InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value; + InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); + }); + } + + private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type); + + private void assertControlPointPosition(int index, Vector2 position) => + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1)); + + private class TestSliderBlueprint : SliderSelectionBlueprint + { + public new SliderBodyPiece BodyPiece => base.BodyPiece; + public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint; + public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint; + public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + + public TestSliderBlueprint(DrawableSlider slider) + : base(slider) + { + } + + protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position); + } + + private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint + { + public new HitCirclePiece CirclePiece => base.CirclePiece; + + public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position) + : base(slider, position) + { + } + } + } +} From 847d44c7d9998b9d2429fcc9bcef967004d314d4 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:13:37 +0100 Subject: [PATCH 21/92] Remove unnecessary length asserts We don't actually care about the length (as this isn't what we're testing), just the type of the slider. --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index e7ea12e538..e47d86428b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -291,8 +291,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointType(0, PathType.Bezier); - // Will be > 10000 if not falling back to Bezier path calc. - assertLength(218.8901f); } [Test] @@ -311,7 +309,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointType(0, PathType.PerfectCurve); - assertLength(212.2276f); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); From 6fbe5300163bea139a466c1be2bb09a63e2bd830 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:14:35 +0100 Subject: [PATCH 22/92] Fix coordinates --- .../TestSceneSliderPlacementBlueprint.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index e47d86428b..6eec5edfbe 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -279,13 +279,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestPlacePerfectCurveSegmentAlmostLinearly() { - addMovementStep(new Vector2(0)); + Vector2 startPosition = new Vector2(200); + + addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(61.382935f, 6.423767f)); + addMovementStep(startPosition + new Vector2(300, 0)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(217.69522f, 22.84021f)); + addMovementStep(startPosition + new Vector2(150, 0.1f)); addClickStep(MouseButton.Right); assertPlaced(true); @@ -296,14 +298,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestPlacePerfectCurveSegmentAlmostLinearlyRecovery() { - addMovementStep(new Vector2(0)); + Vector2 startPosition = new Vector2(200); + + addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(61.382935f, 6.423767f)); + addMovementStep(startPosition + new Vector2(61.382935f, 6.423767f)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(217.69522f, 22.84021f)); // Should convert to bezier - addMovementStep(new Vector2(210.0f, 30.0f)); // Should convert back to perfect + addMovementStep(startPosition + new Vector2(217.69522f, 22.84021f)); // Should convert to bezier + addMovementStep(startPosition + new Vector2(210.0f, 0.0f)); // Should convert back to perfect addClickStep(MouseButton.Right); assertPlaced(true); From 23a4d1c1350ef03a05b3c53b2c73be37ef18a4a7 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:15:28 +0100 Subject: [PATCH 23/92] Shorten recovery test name --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 6eec5edfbe..e42ceaa4ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestPlacePerfectCurveSegmentAlmostLinearlyRecovery() + public void TestPlacePerfectCurveSegmentRecovery() { Vector2 startPosition = new Vector2(200); From 7b395ed783e2811e9e29f73e287965b9bafe8b57 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:15:50 +0100 Subject: [PATCH 24/92] Add exterior arc test --- .../TestSceneSliderPlacementBlueprint.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index e42ceaa4ac..0433acb25c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -284,6 +284,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(startPosition); addClickStep(MouseButton.Left); + addMovementStep(startPosition + new Vector2(61.382935f, 6.423767f)); + addClickStep(MouseButton.Left); + + addMovementStep(startPosition + new Vector2(217.69522f, 22.84021f)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior() + { + Vector2 startPosition = new Vector2(200); + + addMovementStep(startPosition); + addClickStep(MouseButton.Left); + addMovementStep(startPosition + new Vector2(300, 0)); addClickStep(MouseButton.Left); From f80b3ada25f1fe688262c95df3dc65f7862479ce Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:54:48 +0100 Subject: [PATCH 25/92] Add circular arc size tests --- .../TestSceneSliderPlacementBlueprint.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 0433acb25c..20df46cd2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -334,6 +334,40 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.PerfectCurve); } + [Test] + public void TestPlacePerfectCurveSegmentLarge() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(200, 800)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(600, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestPlacePerfectCurveSegmentTooLarge() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(200, 800)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.Bezier); + } + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addClickStep(MouseButton button) From e0240ab9d93db5e4d2467c4002e9a2784ff0e1a1 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:55:34 +0100 Subject: [PATCH 26/92] Increase exterior threshold --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0c9163ae41..c4a16f9c2a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, // where the latter is much faster, hence differing thresholds bool exterior = abSq > acSq || bcSq > acSq; - float threshold = exterior ? 0.05f : 0.001f; + float threshold = exterior ? 0.35f : 0.001f; return Math.Abs(det) >= threshold; } From 415797aadd3c77de155ebdefc53d1c22cb83a723 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 06:01:12 +0100 Subject: [PATCH 27/92] Fix broken control point drag test Broken for 2 reasons: - Assert checks the wrong control point. - The exterior arc is now too big. This fixes both. --- .../Editor/TestSceneSliderControlPointPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index c4f103e40c..c3492fc843 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -112,10 +112,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(new Vector2(350, 0)); assertControlPointType(2, PathType.Bezier); - addMovementStep(new Vector2(150, 150)); + addMovementStep(new Vector2(400, 50)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); - assertControlPointPosition(1, new Vector2(150, 150)); + assertControlPointPosition(4, new Vector2(400, 50)); assertControlPointType(2, PathType.PerfectCurve); } From b4dc35f66ba3c8c6761fdb8dc381057bb13ce73a Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:24:05 +0100 Subject: [PATCH 28/92] Update large arc tests Should now be more robust and readable. --- .../TestSceneSliderControlPointPiece.cs | 4 ++-- .../TestSceneSliderPlacementBlueprint.cs | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index c3492fc843..7ca86335ce 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -112,10 +112,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(new Vector2(350, 0)); assertControlPointType(2, PathType.Bezier); - addMovementStep(new Vector2(400, 50)); + addMovementStep(new Vector2(150, 150)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); - assertControlPointPosition(4, new Vector2(400, 50)); + assertControlPointPosition(4, new Vector2(150, 150)); assertControlPointType(2, PathType.PerfectCurve); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 20df46cd2c..46fe7b7576 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -337,13 +337,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestPlacePerfectCurveSegmentLarge() { - addMovementStep(new Vector2(200)); + Vector2 startPosition = new Vector2(400); + + addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(200, 800)); + addMovementStep(startPosition + new Vector2(240, 240)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(600, 200)); + // Playfield dimensions are 640 x 480. + // So a 480 * 480 bounding box area should be ok. + addMovementStep(startPosition + new Vector2(-240, 240)); addClickStep(MouseButton.Right); assertPlaced(true); @@ -354,13 +358,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestPlacePerfectCurveSegmentTooLarge() { - addMovementStep(new Vector2(200)); + Vector2 startPosition = new Vector2(480, 200); + + addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(200, 800)); + addMovementStep(startPosition + new Vector2(400, 400)); addClickStep(MouseButton.Left); - addMovementStep(new Vector2(400, 200)); + // Playfield dimensions are 640 x 480. + // So an 800 * 800 bounding box area should not be ok. + addMovementStep(startPosition + new Vector2(-400, 400)); addClickStep(MouseButton.Right); assertPlaced(true); From 0f4314c1d8e6741037df4407326d186fb8c3210b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:24:33 +0100 Subject: [PATCH 29/92] Add complete arc test Ensures we can still make smaller circles properly. --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 46fe7b7576..937034fc23 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -376,6 +376,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Bezier); } + [Test] + public void TestPlacePerfectCurveSegmentCompleteArc() + { + addMovementStep(new Vector2(400)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(600, 400)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 410)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.PerfectCurve); + } + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addClickStep(MouseButton button) From 9df059b01decf63403094a3c071a6f54bea7fe7b Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 24 Mar 2021 17:25:28 +0100 Subject: [PATCH 30/92] Add bounding box limit --- .../Components/PathControlPointVisualiser.cs | 101 ++++++++++++++++-- 1 file changed, 90 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index c4a16f9c2a..0270485f31 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -165,11 +166,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// /// Returns whether the given points are arranged in a valid way. Invalid if points /// are almost entirely linear - as this causes the radius to approach infinity, - /// which would exhaust memory when drawing / approximating. + /// or if the bounding box of the arc is too large. /// /// The three points that make up this circular arc segment. /// private bool validCircularArcSegment(IReadOnlyList points) + { + float det = circularArcDeterminant(points); + RectangleF boundingBox = circularArcBoundingBox(points); + + // Determinant limit prevents memory exhaustion as a result of approximating the subpath. + // Bounding box area limit prevents memory exhaustion as a result of drawing the texture. + return Math.Abs(det) > 0.001f && boundingBox.Area < 640 * 480; + } + + /// + /// Computes the determinant of the circular arc segment defined by the given points. + /// + /// The three points defining the circular arc. + /// + private float circularArcDeterminant(IReadOnlyList points) { Vector2 a = points[0]; Vector2 b = points[1]; @@ -181,19 +197,82 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); - float det = (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); + return (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); + } - float acSq = (a - c).LengthSquared; - float abSq = (a - b).LengthSquared; - float bcSq = (b - c).LengthSquared; + /// + /// Computes the bounding box of the circular arc segment defined by the given points. + /// + /// The three points defining the circular arc. + /// + private RectangleF circularArcBoundingBox(IReadOnlyList points) + { + Vector2 a = points[0]; + Vector2 b = points[1]; + Vector2 c = points[2]; - // Exterior = curve wraps around the long way between end-points - // Exterior bottleneck is drawing-related, interior bottleneck is approximation-related, - // where the latter is much faster, hence differing thresholds - bool exterior = abSq > acSq || bcSq > acSq; - float threshold = exterior ? 0.35f : 0.001f; + // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 + float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); + float aSq = a.LengthSquared; + float bSq = b.LengthSquared; + float cSq = c.LengthSquared; - return Math.Abs(det) >= threshold; + Vector2 center = new Vector2( + aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y, + aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d; + + Vector2 dA = a - center; + Vector2 dC = c - center; + + float r = dA.Length; + + double thetaStart = Math.Atan2(dA.Y, dA.X); + double thetaEnd = Math.Atan2(dC.Y, dC.X); + + while (thetaEnd < thetaStart) + thetaEnd += 2 * Math.PI; + + // Decide in which direction to draw the circle, depending on which side of + // AC B lies. + Vector2 orthoAtoC = c - a; + orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X); + bool clockwise = Vector2.Dot(orthoAtoC, b - a) >= 0; + + if (clockwise) + { + if (thetaEnd < thetaStart) + thetaEnd += Math.PI * 2; + } + else + { + if (thetaStart < thetaEnd) + thetaStart += Math.PI * 2; + } + + List boundingBoxPoints = new List(points); + + bool includes0Degrees = 0 > thetaStart && 0 < thetaEnd; + bool includes90Degrees = Math.PI / 2 > thetaStart && Math.PI / 2 < thetaEnd; + bool includes180Degrees = Math.PI > thetaStart && Math.PI < thetaEnd; + bool includes270Degrees = Math.PI * 1.5f > thetaStart && Math.PI * 1.5f < thetaEnd; + + if (!clockwise) + { + includes0Degrees = 0 < thetaStart && 0 > thetaEnd; + includes90Degrees = Math.PI / 2 < thetaStart && Math.PI / 2 > thetaEnd; + includes180Degrees = Math.PI < thetaStart && Math.PI > thetaEnd; + includes270Degrees = Math.PI * 1.5f < thetaStart && Math.PI * 1.5f > thetaEnd; + } + + if (includes0Degrees) boundingBoxPoints.Add(center + new Vector2(1, 0) * r); + if (includes90Degrees) boundingBoxPoints.Add(center + new Vector2(0, 1) * r); + if (includes180Degrees) boundingBoxPoints.Add(center + new Vector2(-1, 0) * r); + if (includes270Degrees) boundingBoxPoints.Add(center + new Vector2(0, -1) * r); + + float width = Math.Abs(boundingBoxPoints.Max(p => p.X) - boundingBoxPoints.Min(p => p.X)); + float height = Math.Abs(boundingBoxPoints.Max(p => p.Y) - boundingBoxPoints.Min(p => p.Y)); + + return new RectangleF(slider.Position, new Vector2(width, height)); } private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) From ce9130ca5065f4278af498e3949049c03d43dbbe Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 25 Mar 2021 17:38:55 +0100 Subject: [PATCH 31/92] Remove determinant limit This has since been added into the framework through https://github.com/ppy/osu-framework/pull/4302 --- .../Components/PathControlPointVisualiser.cs | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0270485f31..348ec30266 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -158,48 +158,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components continue; Vector2[] points = slider.Path.PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); - if (points.Length == 3 && !validCircularArcSegment(points)) + if (points.Length == 3 && circularArcBoundingBox(points).Area >= 640 * 480) segmentStartPoint.Type.Value = PathType.Bezier; } } - /// - /// Returns whether the given points are arranged in a valid way. Invalid if points - /// are almost entirely linear - as this causes the radius to approach infinity, - /// or if the bounding box of the arc is too large. - /// - /// The three points that make up this circular arc segment. - /// - private bool validCircularArcSegment(IReadOnlyList points) - { - float det = circularArcDeterminant(points); - RectangleF boundingBox = circularArcBoundingBox(points); - - // Determinant limit prevents memory exhaustion as a result of approximating the subpath. - // Bounding box area limit prevents memory exhaustion as a result of drawing the texture. - return Math.Abs(det) > 0.001f && boundingBox.Area < 640 * 480; - } - - /// - /// Computes the determinant of the circular arc segment defined by the given points. - /// - /// The three points defining the circular arc. - /// - private float circularArcDeterminant(IReadOnlyList points) - { - Vector2 a = points[0]; - Vector2 b = points[1]; - Vector2 c = points[2]; - - float maxLength = points.Max(p => p.Length); - - Vector2 normA = new Vector2(a.X / maxLength, a.Y / maxLength); - Vector2 normB = new Vector2(b.X / maxLength, b.Y / maxLength); - Vector2 normC = new Vector2(c.X / maxLength, c.Y / maxLength); - - return (normA.X - normB.X) * (normB.Y - normC.Y) - (normB.X - normC.X) * (normA.Y - normB.Y); - } - /// /// Computes the bounding box of the circular arc segment defined by the given points. /// From 51f0477df4744b6702dff18740974e3a17913be3 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Fri, 26 Mar 2021 04:42:46 +0100 Subject: [PATCH 32/92] Move bounding box logic to framework --- .../Components/PathControlPointVisualiser.cs | 81 +------------------ 1 file changed, 4 insertions(+), 77 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 348ec30266..dd39014bb6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -157,87 +158,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (segmentStartPoint.Type.Value != PathType.PerfectCurve) continue; - Vector2[] points = slider.Path.PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); - if (points.Length == 3 && circularArcBoundingBox(points).Area >= 640 * 480) + ReadOnlySpan points = slider.Path.PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (points.Length == 3 && boundingBox.Area >= 640 * 480) segmentStartPoint.Type.Value = PathType.Bezier; } } - /// - /// Computes the bounding box of the circular arc segment defined by the given points. - /// - /// The three points defining the circular arc. - /// - private RectangleF circularArcBoundingBox(IReadOnlyList points) - { - Vector2 a = points[0]; - Vector2 b = points[1]; - Vector2 c = points[2]; - - // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 - float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); - float aSq = a.LengthSquared; - float bSq = b.LengthSquared; - float cSq = c.LengthSquared; - - Vector2 center = new Vector2( - aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y, - aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d; - - Vector2 dA = a - center; - Vector2 dC = c - center; - - float r = dA.Length; - - double thetaStart = Math.Atan2(dA.Y, dA.X); - double thetaEnd = Math.Atan2(dC.Y, dC.X); - - while (thetaEnd < thetaStart) - thetaEnd += 2 * Math.PI; - - // Decide in which direction to draw the circle, depending on which side of - // AC B lies. - Vector2 orthoAtoC = c - a; - orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X); - bool clockwise = Vector2.Dot(orthoAtoC, b - a) >= 0; - - if (clockwise) - { - if (thetaEnd < thetaStart) - thetaEnd += Math.PI * 2; - } - else - { - if (thetaStart < thetaEnd) - thetaStart += Math.PI * 2; - } - - List boundingBoxPoints = new List(points); - - bool includes0Degrees = 0 > thetaStart && 0 < thetaEnd; - bool includes90Degrees = Math.PI / 2 > thetaStart && Math.PI / 2 < thetaEnd; - bool includes180Degrees = Math.PI > thetaStart && Math.PI < thetaEnd; - bool includes270Degrees = Math.PI * 1.5f > thetaStart && Math.PI * 1.5f < thetaEnd; - - if (!clockwise) - { - includes0Degrees = 0 < thetaStart && 0 > thetaEnd; - includes90Degrees = Math.PI / 2 < thetaStart && Math.PI / 2 > thetaEnd; - includes180Degrees = Math.PI < thetaStart && Math.PI > thetaEnd; - includes270Degrees = Math.PI * 1.5f < thetaStart && Math.PI * 1.5f > thetaEnd; - } - - if (includes0Degrees) boundingBoxPoints.Add(center + new Vector2(1, 0) * r); - if (includes90Degrees) boundingBoxPoints.Add(center + new Vector2(0, 1) * r); - if (includes180Degrees) boundingBoxPoints.Add(center + new Vector2(-1, 0) * r); - if (includes270Degrees) boundingBoxPoints.Add(center + new Vector2(0, -1) * r); - - float width = Math.Abs(boundingBoxPoints.Max(p => p.X) - boundingBoxPoints.Min(p => p.X)); - float height = Math.Abs(boundingBoxPoints.Max(p => p.Y) - boundingBoxPoints.Min(p => p.Y)); - - return new RectangleF(slider.Position, new Vector2(width, height)); - } - private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) From 70d5b616f28bb8916bb9dda0a9e1810b942d8afb Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Mon, 29 Mar 2021 15:49:49 +0200 Subject: [PATCH 33/92] Add scaling path type recovery --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 871339ae7b..da484b9782 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnOperationEnded(); referenceOrigin = null; + referencePathTypes = null; } public override bool HandleMovement(MoveSelectionEvent moveEvent) => @@ -43,6 +44,12 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; + /// + /// During a transform, the initial path types of a single selected slider are stored so they + /// can be maintained throughout the operation. + /// + private List referencePathTypes; + public override bool HandleReverse() { var hitObjects = EditorBeatmap.SelectedHitObjects; @@ -141,11 +148,17 @@ namespace osu.Game.Rulesets.Osu.Edit // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) { + referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList(); + Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height); foreach (var point in slider.Path.ControlPoints) point.Position.Value *= pathRelativeDeltaScale; + + // Maintain the path types in case they were defaulted to bezier at some point during scaling + for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) + slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i]; } else { From 1718084dbc55761c9f0a2cc49bccde6d88ccb3f3 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:08:39 +0200 Subject: [PATCH 34/92] Update/remove determinant tests We now only change the path type based on the bounding box. If the control points are too linear, the framework now handles the fallback to Bezier. --- .../TestSceneSliderControlPointPiece.cs | 17 ++----------- .../TestSceneSliderPlacementBlueprint.cs | 25 +++---------------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 7ca86335ce..b4eba7070b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -61,19 +61,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.PerfectCurve); } - [Test] - public void TestDragControlPointAlmostLinearly() - { - moveMouseToControlPoint(1); - AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - - addMovementStep(new Vector2(150, 0.01f)); - AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); - - assertControlPointPosition(1, new Vector2(150, 0.01f)); - assertControlPointType(0, PathType.Bezier); - } - [Test] public void TestDragControlPointAlmostLinearlyExterior() { @@ -93,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToControlPoint(1); AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - addMovementStep(new Vector2(150, 0.01f)); + addMovementStep(new Vector2(400, 0.01f)); assertControlPointType(0, PathType.Bezier); addMovementStep(new Vector2(150, 50)); @@ -109,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor moveMouseToControlPoint(4); AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - addMovementStep(new Vector2(350, 0)); + addMovementStep(new Vector2(350, 0.01f)); assertControlPointType(2, PathType.Bezier); addMovementStep(new Vector2(150, 150)); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 937034fc23..24382d2c65 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -276,25 +276,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.Linear); } - [Test] - public void TestPlacePerfectCurveSegmentAlmostLinearly() - { - Vector2 startPosition = new Vector2(200); - - addMovementStep(startPosition); - addClickStep(MouseButton.Left); - - addMovementStep(startPosition + new Vector2(61.382935f, 6.423767f)); - addClickStep(MouseButton.Left); - - addMovementStep(startPosition + new Vector2(217.69522f, 22.84021f)); - addClickStep(MouseButton.Right); - - assertPlaced(true); - assertControlPointCount(3); - assertControlPointType(0, PathType.Bezier); - } - [Test] public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior() { @@ -322,10 +303,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(startPosition + new Vector2(61.382935f, 6.423767f)); + addMovementStep(startPosition + new Vector2(300, 0)); addClickStep(MouseButton.Left); - addMovementStep(startPosition + new Vector2(217.69522f, 22.84021f)); // Should convert to bezier + addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier addMovementStep(startPosition + new Vector2(210.0f, 0.0f)); // Should convert back to perfect addClickStep(MouseButton.Right); @@ -346,7 +327,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addClickStep(MouseButton.Left); // Playfield dimensions are 640 x 480. - // So a 480 * 480 bounding box area should be ok. + // So a 480 x 480 bounding box should be ok. addMovementStep(startPosition + new Vector2(-240, 240)); addClickStep(MouseButton.Right); From 75b8f2535ff3c024b1cc4ab219989658c7003f1e Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:09:56 +0200 Subject: [PATCH 35/92] Move updatePathTypes to PathControlPointPiece Here we produce a local bound copy of the path version, and bind it to update the path type. This way, if the path version updates (i.e. any control point changes type or position), we check that all control points have a well-defined path. Additionally, if the control point piece is disposed of, the GB should also swoop up the subscription because of the local bound copy. --- .../Components/PathControlPointPiece.cs | 24 +++++++++++++++++++ .../Components/PathControlPointVisualiser.cs | 23 ------------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 4459308ea5..8059fc910c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -3,14 +3,17 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -47,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved] private OsuColour colours { get; set; } + private IBindable sliderVersion; private IBindable sliderPosition; private IBindable sliderScale; private IBindable controlPointPosition; @@ -105,6 +109,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.LoadComplete(); + sliderVersion = slider.Path.Version.GetBoundCopy(); + sliderVersion.BindValueChanged(_ => updatePathType()); + sliderPosition = slider.PositionBindable.GetBoundCopy(); sliderPosition.BindValueChanged(_ => updateMarkerDisplay()); @@ -200,6 +207,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); + /// + /// Handles correction of invalid path types. + /// + private void updatePathType() + { + if (PointsInSegment[0].Type.Value != PathType.PerfectCurve) + return; + + ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); + if (points.Length != 3) + return; + + RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); + if (boundingBox.Width >= 640 || boundingBox.Height >= 480) + PointsInSegment[0].Type.Value = PathType.Bezier; + } + /// /// Updates the state of the circular control point marker. /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index dd39014bb6..ce5dc4855e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -11,12 +11,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -93,8 +91,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components })); Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); - - point.Changed += updatePathTypes; } break; @@ -104,8 +100,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Pieces.RemoveAll(p => p.ControlPoint == point); Connections.RemoveAll(c => c.ControlPoint == point); - - point.Changed -= updatePathTypes; } // If removing before the end of the path, @@ -148,23 +142,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } - /// - /// Handles correction of invalid path types. - /// - private void updatePathTypes() - { - foreach (PathControlPoint segmentStartPoint in slider.Path.ControlPoints.Where(p => p.Type.Value != null)) - { - if (segmentStartPoint.Type.Value != PathType.PerfectCurve) - continue; - - ReadOnlySpan points = slider.Path.PointsInSegment(segmentStartPoint).Select(p => p.Position.Value).ToArray(); - RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); - if (points.Length == 3 && boundingBox.Area >= 640 * 480) - segmentStartPoint.Type.Value = PathType.Bezier; - } - } - private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) From 5022a78e80b1a07c54ab55176570a7e4be2debaa Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:25:46 +0200 Subject: [PATCH 36/92] Check current point instead of start point Since each control point will call this when the path updates, the previous would correct the start segment 3 times instead of just once. This fixes that. --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 8059fc910c..394d2b039d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// private void updatePathType() { - if (PointsInSegment[0].Type.Value != PathType.PerfectCurve) + if (ControlPoint.Type.Value != PathType.PerfectCurve) return; ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); if (boundingBox.Width >= 640 || boundingBox.Height >= 480) - PointsInSegment[0].Type.Value = PathType.Bezier; + ControlPoint.Type.Value = PathType.Bezier; } /// From 7b684339ed64703c169dfccc27c3abfec521e586 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:32:49 +0200 Subject: [PATCH 37/92] Undo public -> internal for `PathControlPoint.Changed` No longer used. --- osu.Game/Rulesets/Objects/PathControlPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index 2e4100ee0b..f11917f4f4 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects /// /// Invoked when any property of this is changed. /// - public event Action Changed; + internal event Action Changed; /// /// Creates a new . From 25afae5671514f88966f06b59eda1ef82b824ecf Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:48:17 +0200 Subject: [PATCH 38/92] Fix broken test case Seems this technically works, but only because of the edge case of being entirely linear, which the framework catches. This fixes that. --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 24382d2c65..462abf2edb 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addClickStep(MouseButton.Left); addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier - addMovementStep(startPosition + new Vector2(210.0f, 0.0f)); // Should convert back to perfect + addMovementStep(startPosition + new Vector2(400.0f, 50.0f)); // Should convert back to perfect addClickStep(MouseButton.Right); assertPlaced(true); From b8479a979f1ae1453e4488ec555a3cdc0e49f082 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 1 Apr 2021 18:06:12 +0200 Subject: [PATCH 39/92] Remove unused blueprint variable --- .../Editor/TestSceneSliderControlPointPiece.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index b4eba7070b..9b67d18db6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -22,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { private Slider slider; private DrawableSlider drawableObject; - private TestSliderBlueprint blueprint; [SetUp] public void Setup() => Schedule(() => @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); Add(drawableObject = new DrawableSlider(slider)); - AddBlueprint(blueprint = new TestSliderBlueprint(drawableObject)); + AddBlueprint(new TestSliderBlueprint(drawableObject)); }); [Test] From 8621a6b4fe8b84e50e2a1d56b25e3bed9824be6f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 1 Apr 2021 20:34:04 +0200 Subject: [PATCH 40/92] Add margin to large segment test Test ran fine on my end, but apparently not on the CI. This should make results a bit more consistent, hopefully. --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 462abf2edb..d2c37061f0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -323,12 +323,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addMovementStep(startPosition); addClickStep(MouseButton.Left); - addMovementStep(startPosition + new Vector2(240, 240)); + addMovementStep(startPosition + new Vector2(220, 220)); addClickStep(MouseButton.Left); // Playfield dimensions are 640 x 480. - // So a 480 x 480 bounding box should be ok. - addMovementStep(startPosition + new Vector2(-240, 240)); + // So a 440 x 440 bounding box should be ok. + addMovementStep(startPosition + new Vector2(-220, 220)); addClickStep(MouseButton.Right); assertPlaced(true); From fe66b84bede18b7a2148c5c4ae1fdea948cad717 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 6 Feb 2021 15:48:14 +1100 Subject: [PATCH 41/92] Implement dynamic previous hitobject retention for Skill class There is no reason we should be limiting skills to knowing only the previous 2 objects. This originally existed as an angle implementation detail of the original pp+ codebase which made its way here, but didn't get used in the same way. --- .../NonVisual/LimitedCapacityStackTest.cs | 115 -------------- osu.Game.Tests/NonVisual/ReverseQueueTest.cs | 143 ++++++++++++++++++ osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 33 +++- .../Difficulty/Utils/LimitedCapacityStack.cs | 92 ----------- .../Rulesets/Difficulty/Utils/ReverseQueue.cs | 110 ++++++++++++++ 5 files changed, 284 insertions(+), 209 deletions(-) delete mode 100644 osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs create mode 100644 osu.Game.Tests/NonVisual/ReverseQueueTest.cs delete mode 100644 osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs deleted file mode 100644 index d5ac38008e..0000000000 --- a/osu.Game.Tests/NonVisual/LimitedCapacityStackTest.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Game.Rulesets.Difficulty.Utils; - -namespace osu.Game.Tests.NonVisual -{ - [TestFixture] - public class LimitedCapacityStackTest - { - private const int capacity = 3; - - private LimitedCapacityStack stack; - - [SetUp] - public void Setup() - { - stack = new LimitedCapacityStack(capacity); - } - - [Test] - public void TestEmptyStack() - { - Assert.AreEqual(0, stack.Count); - - Assert.Throws(() => - { - int unused = stack[0]; - }); - - int count = 0; - foreach (var unused in stack) - count++; - - Assert.AreEqual(0, count); - } - - [TestCase(1)] - [TestCase(2)] - [TestCase(3)] - public void TestInRangeElements(int count) - { - // e.g. 0 -> 1 -> 2 - for (int i = 0; i < count; i++) - stack.Push(i); - - Assert.AreEqual(count, stack.Count); - - // e.g. 2 -> 1 -> 0 (reverse order) - for (int i = 0; i < stack.Count; i++) - Assert.AreEqual(count - 1 - i, stack[i]); - - // e.g. indices 3, 4, 5, 6 (out of range) - for (int i = stack.Count; i < stack.Count + capacity; i++) - { - Assert.Throws(() => - { - int unused = stack[i]; - }); - } - } - - [TestCase(4)] - [TestCase(5)] - [TestCase(6)] - public void TestOverflowElements(int count) - { - // e.g. 0 -> 1 -> 2 -> 3 - for (int i = 0; i < count; i++) - stack.Push(i); - - Assert.AreEqual(capacity, stack.Count); - - // e.g. 3 -> 2 -> 1 (reverse order) - for (int i = 0; i < stack.Count; i++) - Assert.AreEqual(count - 1 - i, stack[i]); - - // e.g. indices 3, 4, 5, 6 (out of range) - for (int i = stack.Count; i < stack.Count + capacity; i++) - { - Assert.Throws(() => - { - int unused = stack[i]; - }); - } - } - - [TestCase(1)] - [TestCase(2)] - [TestCase(3)] - [TestCase(4)] - [TestCase(5)] - [TestCase(6)] - public void TestEnumerator(int count) - { - // e.g. 0 -> 1 -> 2 -> 3 - for (int i = 0; i < count; i++) - stack.Push(i); - - int enumeratorCount = 0; - int expectedValue = count - 1; - - foreach (var item in stack) - { - Assert.AreEqual(expectedValue, item); - enumeratorCount++; - expectedValue--; - } - - Assert.AreEqual(stack.Count, enumeratorCount); - } - } -} diff --git a/osu.Game.Tests/NonVisual/ReverseQueueTest.cs b/osu.Game.Tests/NonVisual/ReverseQueueTest.cs new file mode 100644 index 0000000000..93cd9403ce --- /dev/null +++ b/osu.Game.Tests/NonVisual/ReverseQueueTest.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Rulesets.Difficulty.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class ReverseQueueTest + { + private ReverseQueue queue; + + [SetUp] + public void Setup() + { + queue = new ReverseQueue(4); + } + + [Test] + public void TestEmptyQueue() + { + Assert.AreEqual(0, queue.Count); + + Assert.Throws(() => + { + char unused = queue[0]; + }); + + int count = 0; + foreach (var unused in queue) + count++; + + Assert.AreEqual(0, count); + } + + [Test] + public void TestEnqueue() + { + // Assert correct values and reverse index after enqueueing + queue.Enqueue('a'); + queue.Enqueue('b'); + queue.Enqueue('c'); + + Assert.AreEqual('c', queue[0]); + Assert.AreEqual('b', queue[1]); + Assert.AreEqual('a', queue[2]); + + // Assert correct values and reverse index after enqueueing beyond initial capacity of 4 + queue.Enqueue('d'); + queue.Enqueue('e'); + queue.Enqueue('f'); + + Assert.AreEqual('f', queue[0]); + Assert.AreEqual('e', queue[1]); + Assert.AreEqual('d', queue[2]); + Assert.AreEqual('c', queue[3]); + Assert.AreEqual('b', queue[4]); + Assert.AreEqual('a', queue[5]); + } + + [Test] + public void TestDequeue() + { + queue.Enqueue('a'); + queue.Enqueue('b'); + queue.Enqueue('c'); + queue.Enqueue('d'); + queue.Enqueue('e'); + queue.Enqueue('f'); + + // Assert correct item return and no longer in queue after dequeueing + Assert.AreEqual('a', queue[5]); + var dequeuedItem = queue.Dequeue(); + + Assert.AreEqual('a', dequeuedItem); + Assert.AreEqual(5, queue.Count); + Assert.AreEqual('f', queue[0]); + Assert.AreEqual('b', queue[4]); + Assert.Throws(() => + { + char unused = queue[5]; + }); + + // Assert correct state after enough enqueues and dequeues to wrap around array (queue.start = 0 again) + queue.Enqueue('g'); + queue.Enqueue('h'); + queue.Enqueue('i'); + queue.Dequeue(); + queue.Dequeue(); + queue.Dequeue(); + queue.Dequeue(); + queue.Dequeue(); + queue.Dequeue(); + queue.Dequeue(); + + Assert.AreEqual(1, queue.Count); + Assert.AreEqual('i', queue[0]); + } + + [Test] + public void TestClear() + { + queue.Enqueue('a'); + queue.Enqueue('b'); + queue.Enqueue('c'); + queue.Enqueue('d'); + queue.Enqueue('e'); + queue.Enqueue('f'); + + // Assert queue is empty after clearing + queue.Clear(); + + Assert.AreEqual(0, queue.Count); + Assert.Throws(() => + { + char unused = queue[0]; + }); + } + + [Test] + public void TestEnumerator() + { + queue.Enqueue('a'); + queue.Enqueue('b'); + queue.Enqueue('c'); + queue.Enqueue('d'); + queue.Enqueue('e'); + queue.Enqueue('f'); + + char[] expectedValues = { 'f', 'e', 'd', 'c', 'b', 'a' }; + int expectedValueIndex = 0; + + // Assert items are enumerated in correct order + foreach (var item in queue) + { + Assert.AreEqual(expectedValues[expectedValueIndex], item); + expectedValueIndex++; + } + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 126e30ed73..caa94a5355 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -40,7 +40,14 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// s that were processed previously. They can affect the strain values of the following objects. /// - protected readonly LimitedCapacityStack Previous = new LimitedCapacityStack(2); // Contained objects not used yet + protected readonly ReverseQueue Previous; + + /// + /// Soft capacity of the queue. + /// will automatically resize if it exceeds capacity, but will do so at a very slight performance impact. + /// The actual capacity will be set to this value + 1 to allow for storage of the current object before the next can be processed. + /// + protected virtual int PreviousCollectionSoftCapacity => 1; /// /// The current strain level. @@ -61,6 +68,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills protected Skill(Mod[] mods) { this.mods = mods; + Previous = new ReverseQueue(PreviousCollectionSoftCapacity + 1); } /// @@ -68,12 +76,33 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public void Process(DifficultyHitObject current) { + RemoveExtraneousHistory(current); + CurrentStrain *= strainDecay(current.DeltaTime); CurrentStrain += StrainValueOf(current) * SkillMultiplier; currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak); - Previous.Push(current); + AddToHistory(current); + } + + /// + /// Remove objects from that are no longer needed for calculations from the current object onwards. + /// + /// The to be processed. + protected virtual void RemoveExtraneousHistory(DifficultyHitObject current) + { + while (Previous.Count > 1) + Previous.Dequeue(); + } + + /// + /// Add the current to the queue (if required). + /// + /// The that was just processed. + protected virtual void AddToHistory(DifficultyHitObject current) + { + Previous.Enqueue(current); } /// diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs deleted file mode 100644 index 1fc5abce90..0000000000 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityStack.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Difficulty.Utils -{ - /// - /// An indexed stack with limited depth. Indexing starts at the top of the stack. - /// - public class LimitedCapacityStack : IEnumerable - { - /// - /// The number of elements in the stack. - /// - public int Count { get; private set; } - - private readonly T[] array; - private readonly int capacity; - private int marker; // Marks the position of the most recently added item. - - /// - /// Constructs a new . - /// - /// The number of items the stack can hold. - public LimitedCapacityStack(int capacity) - { - if (capacity < 0) - throw new ArgumentOutOfRangeException(nameof(capacity)); - - this.capacity = capacity; - array = new T[capacity]; - marker = capacity; // Set marker to the end of the array, outside of the indexed range by one. - } - - /// - /// Retrieves the item at an index in the stack. - /// - /// The index of the item to retrieve. The top of the stack is returned at index 0. - public T this[int i] - { - get - { - if (i < 0 || i > Count - 1) - throw new ArgumentOutOfRangeException(nameof(i)); - - i += marker; - if (i > capacity - 1) - i -= capacity; - - return array[i]; - } - } - - /// - /// Pushes an item to this . - /// - /// The item to push. - public void Push(T item) - { - // Overwrite the oldest item instead of shifting every item by one with every addition. - if (marker == 0) - marker = capacity - 1; - else - --marker; - - array[marker] = item; - - if (Count < capacity) - ++Count; - } - - /// - /// Returns an enumerator which enumerates items in the history starting from the most recently added one. - /// - public IEnumerator GetEnumerator() - { - for (int i = marker; i < capacity; ++i) - yield return array[i]; - - if (Count == capacity) - { - for (int i = 0; i < marker; ++i) - yield return array[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs new file mode 100644 index 0000000000..08b2936876 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// An indexed queue where items are indexed beginning from the end instead of the start. + /// + public class ReverseQueue : IEnumerable + { + /// + /// The number of elements in the . + /// + public int Count { get; private set; } + + private T[] items; + private int capacity; + private int start; + + public ReverseQueue(int initialCapacity) + { + if (initialCapacity <= 0) + throw new ArgumentOutOfRangeException(nameof(initialCapacity)); + + items = new T[initialCapacity]; + capacity = initialCapacity; + start = 0; + Count = 0; + } + + /// + /// Retrieves the item at an index in the . + /// + /// The index of the item to retrieve. The most recently enqueued item is at index 0. + public T this[int index] + { + get + { + if (index < 0 || index > Count - 1) + throw new ArgumentOutOfRangeException(nameof(index)); + + int reverseIndex = Count - 1 - index; + return items[(start + reverseIndex) % capacity]; + } + } + + /// + /// Enqueues an item to this . + /// + /// The item to enqueue. + public void Enqueue(T item) + { + if (Count == capacity) + { + // Double the buffer size + var buffer = new T[capacity * 2]; + + // Copy items to new queue + for (int i = 0; i < Count; i++) + { + buffer[i] = items[(start + i) % capacity]; + } + + // Replace array with new buffer + items = buffer; + capacity *= 2; + start = 0; + } + + items[(start + Count) % capacity] = item; + Count++; + } + + /// + /// Dequeues an item from the and returns it. + /// + /// The item dequeued from the . + public T Dequeue() + { + var item = items[start]; + start = (start + 1) % capacity; + Count--; + return item; + } + + /// + /// Clears the of all items. + /// + public void Clear() + { + start = 0; + Count = 0; + } + + /// + /// Returns an enumerator which enumerates items in the starting from the most recently enqueued one. + /// + public IEnumerator GetEnumerator() + { + for (int i = Count - 1; i >= 0; i--) + yield return items[(start + i) % capacity]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} From ffe7edc16ac13f4460d3dbfc327d1622bfb7e32e Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Tue, 6 Apr 2021 11:06:10 +1000 Subject: [PATCH 42/92] Update xmldocs Co-authored-by: Dan Balasescu --- osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs index 08b2936876..e94fbd4e0f 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Difficulty.Utils { /// - /// An indexed queue where items are indexed beginning from the end instead of the start. + /// An indexed queue where items are indexed beginning from the most recently enqueued item. /// public class ReverseQueue : IEnumerable { @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils } /// - /// Dequeues an item from the and returns it. + /// Dequeues the least recently enqueued item from the and returns it. /// /// The item dequeued from the . public T Dequeue() @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils } /// - /// Returns an enumerator which enumerates items in the starting from the most recently enqueued one. + /// Returns an enumerator which enumerates items in the starting from the most recently enqueued item. /// public IEnumerator GetEnumerator() { From 65f93d6f9d9fbc8a9b3c68b10e6303ed425aaf71 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Tue, 6 Apr 2021 11:30:58 +1000 Subject: [PATCH 43/92] Add more descriptive xmldoc for ReverseQueue --- osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs index e94fbd4e0f..f104b8105a 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Difficulty.Utils { /// /// An indexed queue where items are indexed beginning from the most recently enqueued item. + /// Enqueuing an item pushes all existing indexes up by one and inserts the item at index 0. + /// Dequeuing an item removes the item from the highest index and returns it. /// public class ReverseQueue : IEnumerable { From 5cd43b3a7fc2f267628e5b1b979affa52d4f9e2d Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Tue, 6 Apr 2021 11:53:31 +1000 Subject: [PATCH 44/92] Set default history retention to 0 for Skill and override in StrainSkill Some skills might not even require history retention, so why waste the allocations? --- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 10 +++++----- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 94d823f2ad..ebd389d823 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -22,8 +22,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// Soft capacity of the queue. /// will automatically resize if it exceeds capacity, but will do so at a very slight performance impact. /// The actual capacity will be set to this value + 1 to allow for storage of the current object before the next can be processed. + /// Setting to zero (default) will cause to be uninstanciated. /// - protected virtual int PreviousCollectionSoftCapacity => 1; + protected virtual int PreviousCollectionSoftCapacity => 0; /// /// Mods for use in skill calculations. @@ -35,7 +36,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills protected Skill(Mod[] mods) { this.mods = mods; - Previous = new ReverseQueue(PreviousCollectionSoftCapacity + 1); + + if (PreviousCollectionSoftCapacity > 0) + Previous = new ReverseQueue(PreviousCollectionSoftCapacity + 1); } internal void ProcessInternal(DifficultyHitObject current) @@ -51,8 +54,6 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// The to be processed. protected virtual void RemoveExtraneousHistory(DifficultyHitObject current) { - while (Previous.Count > 1) - Previous.Dequeue(); } /// @@ -61,7 +62,6 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// The that was just processed. protected virtual void AddToHistory(DifficultyHitObject current) { - Previous.Enqueue(current); } /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 71cee36812..16816be35c 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected abstract double SkillMultiplier { get; } + protected override int PreviousCollectionSoftCapacity => 1; + /// /// Determines how quickly strain decays for the given skill. /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. @@ -52,6 +54,17 @@ namespace osu.Game.Rulesets.Difficulty.Skills { } + protected override void RemoveExtraneousHistory(DifficultyHitObject current) + { + while (Previous.Count > 1) + Previous.Dequeue(); + } + + protected override void AddToHistory(DifficultyHitObject current) + { + Previous.Enqueue(current); + } + /// /// Process a and update current strain values accordingly. /// From 37e30b00bf6d6a494847565036d3538fb1f1e939 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 6 Apr 2021 16:39:02 +0900 Subject: [PATCH 45/92] Refactor to keep a consistent API --- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 32 ++++--------------- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 13 -------- 2 files changed, 7 insertions(+), 38 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index ebd389d823..9f0fb987a7 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -19,12 +19,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills protected readonly ReverseQueue Previous; /// - /// Soft capacity of the queue. - /// will automatically resize if it exceeds capacity, but will do so at a very slight performance impact. - /// The actual capacity will be set to this value + 1 to allow for storage of the current object before the next can be processed. - /// Setting to zero (default) will cause to be uninstanciated. + /// Number of previous s to keep inside the queue. /// - protected virtual int PreviousCollectionSoftCapacity => 0; + protected virtual int HistoryLength => 1; /// /// Mods for use in skill calculations. @@ -36,32 +33,17 @@ namespace osu.Game.Rulesets.Difficulty.Skills protected Skill(Mod[] mods) { this.mods = mods; - - if (PreviousCollectionSoftCapacity > 0) - Previous = new ReverseQueue(PreviousCollectionSoftCapacity + 1); + Previous = new ReverseQueue(HistoryLength + 1); } internal void ProcessInternal(DifficultyHitObject current) { - RemoveExtraneousHistory(current); + while (Previous.Count > HistoryLength) + Previous.Dequeue(); + Process(current); - AddToHistory(current); - } - /// - /// Remove objects from that are no longer needed for calculations from the current object onwards. - /// - /// The to be processed. - protected virtual void RemoveExtraneousHistory(DifficultyHitObject current) - { - } - - /// - /// Add the current to the queue (if required). - /// - /// The that was just processed. - protected virtual void AddToHistory(DifficultyHitObject current) - { + Previous.Enqueue(current); } /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 16816be35c..71cee36812 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected abstract double SkillMultiplier { get; } - protected override int PreviousCollectionSoftCapacity => 1; - /// /// Determines how quickly strain decays for the given skill. /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. @@ -54,17 +52,6 @@ namespace osu.Game.Rulesets.Difficulty.Skills { } - protected override void RemoveExtraneousHistory(DifficultyHitObject current) - { - while (Previous.Count > 1) - Previous.Dequeue(); - } - - protected override void AddToHistory(DifficultyHitObject current) - { - Previous.Enqueue(current); - } - /// /// Process a and update current strain values accordingly. /// From d5ba77b2c2b5984b279c7809a1f58878c92e0936 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 6 Apr 2021 21:22:28 +0900 Subject: [PATCH 46/92] Add spectating user state --- osu.Game/Online/Multiplayer/MultiplayerUserState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs index e54c71cd85..c467ff84bb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs @@ -55,5 +55,10 @@ namespace osu.Game.Online.Multiplayer /// The user is currently viewing results. This is a reserved state, and is set by the server. /// Results, + + /// + /// The user is currently spectating this room. + /// + Spectating } } From 6de91d7b6bb38caa25ea455a2868b548b083d5ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 6 Apr 2021 21:37:21 +0900 Subject: [PATCH 47/92] Add spectate button + test --- .../TestSceneMultiplayerSpectateButton.cs | 53 +++++++++++ .../Multiplayer/StatefulMultiplayerClient.cs | 27 ++++++ .../Match/MultiplayerSpectateButton.cs | 92 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs new file mode 100644 index 0000000000..730fee2fba --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene + { + private MultiplayerSpectateButton button; + private IDisposable readyClickOperation; + + [SetUp] + public new void Setup() => Schedule(() => + { + Child = button = new MultiplayerSpectateButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + OnSpectateClick = async () => + { + readyClickOperation = OngoingOperationTracker.BeginOperation(); + await Client.ToggleSpectate(); + readyClickOperation.Dispose(); + } + }; + }); + + [TestCase(MultiplayerUserState.Idle)] + [TestCase(MultiplayerUserState.Ready)] + public void TestToggleWhenIdle(MultiplayerUserState initialState) + { + addClickButtonStep(); + AddAssert("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating); + + addClickButtonStep(); + AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + private void addClickButtonStep() => AddStep("click button", () => + { + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + } +} diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 0f7050596f..2ddc10db0f 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -249,6 +249,33 @@ namespace osu.Game.Online.Multiplayer } } + /// + /// Toggles the 's spectating state. + /// + /// If a toggle of the spectating state is not valid at this time. + public async Task ToggleSpectate() + { + var localUser = LocalUser; + + if (localUser == null) + return; + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + case MultiplayerUserState.Ready: + await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false); + return; + + case MultiplayerUserState.Spectating: + await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false); + return; + + default: + throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}"); + } + } + public abstract Task TransferHost(int userId); public abstract Task ChangeSettings(MultiplayerRoomSettings settings); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs new file mode 100644 index 0000000000..50be7719d9 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public class MultiplayerSpectateButton : MultiplayerRoomComposite + { + public Action OnSpectateClick + { + set => button.Action = value; + } + + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + private IBindable operationInProgress; + + private readonly ButtonWithTrianglesExposed button; + + public MultiplayerSpectateButton() + { + InternalChild = button = new ButtonWithTrianglesExposed + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Enabled = { Value = true }, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + operationInProgress.BindValueChanged(_ => updateState()); + } + + protected override void OnRoomUpdated() + { + base.OnRoomUpdated(); + + updateState(); + } + + private void updateState() + { + var localUser = Client.LocalUser; + + if (localUser == null) + return; + + Debug.Assert(Room != null); + + switch (localUser.State) + { + default: + button.Text = "Spectate"; + button.BackgroundColour = colours.Blue; + button.Triangles.ColourDark = colours.Blue; + button.Triangles.ColourLight = colours.BlueLight; + break; + + case MultiplayerUserState.Spectating: + button.Text = "Stop spectating"; + button.BackgroundColour = colours.Red; + button.Triangles.ColourDark = colours.Red; + button.Triangles.ColourLight = colours.RedLight; + break; + } + + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + } + + private class ButtonWithTrianglesExposed : TriangleButton + { + public new Triangles Triangles => base.Triangles; + } + } +} From 1f57b6884d0e944907cf4ce6f2cc12c157fc32c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 16:19:10 +0900 Subject: [PATCH 48/92] Add ready button to test scene --- .../TestSceneMultiplayerSpectateButton.cs | 121 ++++++++++++++++-- 1 file changed, 108 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 730fee2fba..52a1909396 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -2,10 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -13,22 +25,96 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene { - private MultiplayerSpectateButton button; + private MultiplayerSpectateButton spectateButton; + private MultiplayerReadyButton readyButton; + + private readonly Bindable selectedItem = new Bindable(); + + private BeatmapSetInfo importedSet; + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private IDisposable readyClickOperation; + protected override Container Content => content; + private readonly Container content; + + public TestSceneMultiplayerSpectateButton() + { + base.Content.Add(content = new Container + { + RelativeSizeAxes = Axes.Both + }); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + return dependencies; + } + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + + var beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem } }; + base.Content.Add(beatmapTracker); + Dependencies.Cache(beatmapTracker); + + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + } + [SetUp] public new void Setup() => Schedule(() => { - Child = button = new MultiplayerSpectateButton + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); + selectedItem.Value = new PlaylistItem { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 50), - OnSpectateClick = async () => + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + }; + + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - readyClickOperation = OngoingOperationTracker.BeginOperation(); - await Client.ToggleSpectate(); - readyClickOperation.Dispose(); + spectateButton = new MultiplayerSpectateButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + OnSpectateClick = async () => + { + readyClickOperation = OngoingOperationTracker.BeginOperation(); + await Client.ToggleSpectate(); + readyClickOperation.Dispose(); + } + }, + readyButton = new MultiplayerReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + OnReadyClick = async () => + { + readyClickOperation = OngoingOperationTracker.BeginOperation(); + + if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + { + await Client.StartMatch(); + return; + } + + await Client.ToggleReady(); + readyClickOperation.Dispose(); + } + } } }; }); @@ -37,17 +123,26 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(MultiplayerUserState.Ready)] public void TestToggleWhenIdle(MultiplayerUserState initialState) { - addClickButtonStep(); + addClickSpectateButtonStep(); AddAssert("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating); - addClickButtonStep(); + addClickSpectateButtonStep(); AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); } - private void addClickButtonStep() => AddStep("click button", () => + private void addClickSpectateButtonStep() => AddStep("click spectate button", () => { - InputManager.MoveMouseTo(button); + InputManager.MoveMouseTo(spectateButton); InputManager.Click(MouseButton.Left); }); + + private void addClickReadyButtonStep() => AddStep("click ready button", () => + { + InputManager.MoveMouseTo(readyButton); + InputManager.Click(MouseButton.Left); + }); + + private void assertReadyButtonEnablement(bool shouldBeEnabled) + => AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } } From 6be9c9f0f4e8188d4e50b55e26f0025cc2d06292 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 16:35:36 +0900 Subject: [PATCH 49/92] Link up ready button to spectate state --- .../TestSceneMultiplayerSpectateButton.cs | 51 +++++++++++++++++++ .../Match/MultiplayerReadyButton.cs | 11 +++- .../Multiplayer/TestMultiplayerClient.cs | 6 +++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 52a1909396..631511eac5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -18,6 +18,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK; using osuTK.Input; @@ -119,6 +120,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); + [Test] + public void TestEnabledWhenRoomOpen() + { + assertSpectateButtonEnablement(true); + } + [TestCase(MultiplayerUserState.Idle)] [TestCase(MultiplayerUserState.Ready)] public void TestToggleWhenIdle(MultiplayerUserState initialState) @@ -130,6 +137,47 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); } + [TestCase(MultiplayerRoomState.WaitingForLoad)] + [TestCase(MultiplayerRoomState.Playing)] + [TestCase(MultiplayerRoomState.Closed)] + public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState) + { + AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState)); + assertSpectateButtonEnablement(false); + } + + [Test] + public void TestReadyButtonDisabledWhenHostAndNoReadyUsers() + { + addClickSpectateButtonStep(); + assertReadyButtonEnablement(false); + } + + [Test] + public void TestReadyButtonEnabledWhenHostAndUsersReady() + { + AddStep("add user", () => Client.AddUser(new User { Id = 55 })); + AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready)); + + addClickSpectateButtonStep(); + assertReadyButtonEnablement(true); + } + + [Test] + public void TestReadyButtonDisabledWhenNotHostAndUsersReady() + { + AddStep("add user and transfer host", () => + { + Client.AddUser(new User { Id = 55 }); + Client.TransferHost(55); + }); + + AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready)); + + addClickSpectateButtonStep(); + assertReadyButtonEnablement(false); + } + private void addClickSpectateButtonStep() => AddStep("click spectate button", () => { InputManager.MoveMouseTo(spectateButton); @@ -142,6 +190,9 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); + private void assertSpectateButtonEnablement(bool shouldBeEnabled) + => AddAssert($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); + private void assertReadyButtonEnablement(bool shouldBeEnabled) => AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index c9fb234ccc..6919be2d56 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -78,8 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Debug.Assert(Room != null); int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating); - string countText = $"({newCountReady} / {Room.Users.Count} ready)"; + string countText = $"({newCountReady} / {newCountTotal} ready)"; switch (localUser.State) { @@ -88,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match updateButtonColour(true); break; + case MultiplayerUserState.Spectating: case MultiplayerUserState.Ready: if (Room?.Host?.Equals(localUser) == true) { @@ -105,6 +107,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + // When the local user is the host and spectating the match, the "start match" state should be enabled. + if (localUser.State == MultiplayerUserState.Spectating) + { + button.Enabled.Value &= Room?.Host?.Equals(localUser) == true; + button.Enabled.Value &= newCountReady > 0; + } + if (newCountReady != countReady) { countReady = newCountReady; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 09fcc1ff47..4c954c7d27 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + public void ChangeRoomState(MultiplayerRoomState newState) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).RoomStateChanged(newState); + } + public void ChangeUserState(int userId, MultiplayerUserState newState) { Debug.Assert(Room != null); From f5667125a017e56d153c388638c2f2a13d624389 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 16:37:43 +0900 Subject: [PATCH 50/92] Remove unnecessary method --- .../Multiplayer/TestSceneMultiplayerSpectateButton.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 631511eac5..8fe1f99e1d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -184,12 +184,6 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - private void addClickReadyButtonStep() => AddStep("click ready button", () => - { - InputManager.MoveMouseTo(readyButton); - InputManager.Click(MouseButton.Left); - }); - private void assertSpectateButtonEnablement(bool shouldBeEnabled) => AddAssert($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType().Single().Enabled.Value == shouldBeEnabled); From c744f77cfafdab402bbc1e34793402baf6c0995c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 16:40:24 +0900 Subject: [PATCH 51/92] Add participant panel state --- .../Multiplayer/TestSceneMultiplayerParticipantsList.cs | 7 +++++++ .../OnlinePlay/Multiplayer/Participants/StateDisplay.cs | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index e713cff233..7f8f04b718 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -126,6 +126,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); } + [Test] + public void TestToggleSpectateState() + { + AddStep("make user spectating", () => Client.ChangeState(MultiplayerUserState.Spectating)); + AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + } + [Test] public void TestCrownChangesStateWhenHostTransferred() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index c571b51c83..e6a407acec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -135,6 +135,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants icon.Colour = colours.BlueLighter; break; + case MultiplayerUserState.Spectating: + text.Text = "spectating"; + icon.Icon = FontAwesome.Solid.Eye; + icon.Colour = colours.BlueLight; + break; + default: throw new ArgumentOutOfRangeException(nameof(state), state, null); } From abd637ffaa5345f71f248823de47a3d5d4e805f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 17:35:18 +0900 Subject: [PATCH 52/92] Add button to footer --- .../TestSceneMultiplayerMatchFooter.cs | 27 +++++++++++++ .../Match/MultiplayerMatchFooter.cs | 40 ++++++++++++++++--- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs new file mode 100644 index 0000000000..08c94b8135 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene + { + [Cached] + private readonly OnlinePlayBeatmapAvailablilityTracker availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker(); + + [BackgroundDependencyLoader] + private void load() + { + Child = new MultiplayerMatchFooter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 50 + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index fdc1ae9d3c..d4f5428bfb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -8,21 +8,28 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchFooter : CompositeDrawable { public const float HEIGHT = 50; + private const float ready_button_width = 600; + private const float spectate_button_width = 200; public Action OnReadyClick { set => readyButton.OnReadyClick = value; } + public Action OnSpectateClick + { + set => spectateButton.OnSpectateClick = value; + } + private readonly Drawable background; private readonly MultiplayerReadyButton readyButton; + private readonly MultiplayerSpectateButton spectateButton; public MultiplayerMatchFooter() { @@ -32,11 +39,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - readyButton = new MultiplayerReadyButton + new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(600, 50), + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + null, + spectateButton = new MultiplayerSpectateButton + { + RelativeSizeAxes = Axes.Both, + }, + null, + readyButton = new MultiplayerReadyButton + { + RelativeSizeAxes = Axes.Both, + }, + null + } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(maxSize: spectate_button_width), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(maxSize: ready_button_width), + new Dimension() + } } }; } From 93c5935ebc4adc5989122b0f502fab6c44dd5064 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 20:46:30 +0900 Subject: [PATCH 53/92] Add match subscreen support + test --- .../TestSceneMultiplayerMatchSubScreen.cs | 61 +++++++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 +++++- .../Multiplayer/TestMultiplayerClient.cs | 3 + 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8869718fd1..6f8ec7fcfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,13 +3,21 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer @@ -18,11 +26,25 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; + private BeatmapManager beatmaps; + private RulesetStore rulesets; + private BeatmapSetInfo importedSet; + public TestSceneMultiplayerMatchSubScreen() : base(false) { } + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); + + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + } + [SetUp] public new void Setup() => Schedule(() => { @@ -73,5 +95,44 @@ namespace osu.Game.Tests.Visual.Multiplayer AddWaitStep("wait", 10); } + + [Test] + public void TestStartMatchWhileSpectating() + { + AddStep("set playlist", () => + { + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddStep("click create button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("join other user (ready)", () => + { + Client.AddUser(new User { Id = 55 }); + Client.ChangeUserState(55, MultiplayerUserState.Ready); + }); + + AddStep("click spectate button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("click ready button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ceeee67806..90cef0107c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -221,7 +221,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new MultiplayerMatchFooter { - OnReadyClick = onReadyClick + OnReadyClick = onReadyClick, + OnSpectateClick = onSpectateClick } } }, @@ -363,7 +364,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(readyClickOperation == null); readyClickOperation = ongoingOperationTracker.BeginOperation(); - if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready) + if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating)) { client.StartMatch() .ContinueWith(t => @@ -390,6 +391,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private void onSpectateClick() + { + Debug.Assert(readyClickOperation == null); + readyClickOperation = ongoingOperationTracker.BeginOperation(); + + client.ToggleSpectate().ContinueWith(t => endOperation()); + + void endOperation() + { + readyClickOperation?.Dispose(); + readyClickOperation = null; + } + } + private void onRoomUpdated() { // user mods may have changed. diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 4c954c7d27..b5cd3dad02 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -77,6 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerUserState.Loaded: if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) { + ChangeRoomState(MultiplayerRoomState.Playing); foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) ChangeUserState(u.UserID, MultiplayerUserState.Playing); @@ -88,6 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case MultiplayerUserState.FinishedPlay: if (Room.Users.All(u => u.State != MultiplayerUserState.Playing)) { + ChangeRoomState(MultiplayerRoomState.Open); foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) ChangeUserState(u.UserID, MultiplayerUserState.Results); @@ -179,6 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); + ChangeRoomState(MultiplayerRoomState.WaitingForLoad); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); From 2791d454d22a8d61e254531635d7a6d7c8bb746e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 22:21:22 +0900 Subject: [PATCH 54/92] Don't send spectating user state yet --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 4529dfd0a7..37e11cc576 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -96,6 +96,9 @@ namespace osu.Game.Online.Multiplayer if (!IsConnected.Value) return Task.CompletedTask; + if (newState == MultiplayerUserState.Spectating) + return Task.CompletedTask; // Not supported yet. + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); } From 214813154b775f1fa8c3a075e55690ce8ef2ee0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 7 Apr 2021 22:28:22 +0900 Subject: [PATCH 55/92] Fix class name --- .../Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs index 08c94b8135..6b03b53b4b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene { [Cached] - private readonly OnlinePlayBeatmapAvailablilityTracker availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker(); + private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 8fe1f99e1d..e65e4a68a7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); - var beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem } }; + var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } }; base.Content.Add(beatmapTracker); Dependencies.Cache(beatmapTracker); From 9d0293070971e17132b201448afbb42c5f40b295 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 7 Apr 2021 17:18:55 +0200 Subject: [PATCH 56/92] Add regression test for type changes --- .../TestSceneSliderControlPointPiece.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 9b67d18db6..d7dfc3bd42 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -105,6 +105,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(2, PathType.PerfectCurve); } + [Test] + public void TestDragControlPointPathAfterChangingType() + { + AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier); + AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); + AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve); + + moveMouseToControlPoint(4); + AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); + + assertControlPointType(3, PathType.PerfectCurve); + + addMovementStep(new Vector2(350, 0.01f)); + AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertControlPointPosition(4, new Vector2(350, 0.01f)); + assertControlPointType(3, PathType.Bezier); + } + private void addMovementStep(Vector2 relativePosition) { AddStep($"move mouse to {relativePosition}", () => From b8ab1c768253402bdfb3644808cbfde05234ef07 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Wed, 7 Apr 2021 17:19:12 +0200 Subject: [PATCH 57/92] Track path type changes for `PointsInSegment` --- .../Sliders/Components/PathControlPointPiece.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 394d2b039d..5fcf0656c5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -50,6 +50,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved] private OsuColour colours { get; set; } + private readonly List> pathTypes; + private IBindable sliderVersion; private IBindable sliderPosition; private IBindable sliderScale; @@ -59,8 +61,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; + pathTypes = new List>(); + slider.Path.ControlPoints.BindCollectionChanged((_, args) => { + pathTypes.Clear(); + + foreach (var point in slider.Path.ControlPoints) + { + IBindable boundTypeCopy = point.Type.GetBoundCopy(); + pathTypes.Add(boundTypeCopy); + boundTypeCopy.BindValueChanged(_ => PointsInSegment = slider.Path.PointsInSegment(controlPoint)); + } + PointsInSegment = slider.Path.PointsInSegment(controlPoint); }, runOnceImmediately: true); From d1d56c636a04a48e5cafdc3cf0cb1d1996d2162e Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 01:43:06 +0200 Subject: [PATCH 58/92] Convert `pathTypes` to local variable Not entirely sure what holds the reference to pathTypes now (the binding to`slider.Path.ControlPoints` maybe?), but this does seem to work still. --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 5fcf0656c5..bb50fec5dc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -50,8 +50,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved] private OsuColour colours { get; set; } - private readonly List> pathTypes; - private IBindable sliderVersion; private IBindable sliderPosition; private IBindable sliderScale; @@ -61,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; - pathTypes = new List>(); + var pathTypes = new List>(); slider.Path.ControlPoints.BindCollectionChanged((_, args) => { From d6490899e2946294bbdab7f0d0db6a5bd014ce5d Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 03:21:56 +0200 Subject: [PATCH 59/92] Simplify slider path bindings Adds a slight performance overhead, but solves the memory leak and makes the code much easier to follow. --- .../Components/PathControlPointPiece.cs | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index bb50fec5dc..20d4fe4ea9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -50,7 +50,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components [Resolved] private OsuColour colours { get; set; } - private IBindable sliderVersion; private IBindable sliderPosition; private IBindable sliderScale; private IBindable controlPointPosition; @@ -59,20 +58,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { this.slider = slider; ControlPoint = controlPoint; - var pathTypes = new List>(); - slider.Path.ControlPoints.BindCollectionChanged((_, args) => + slider.Path.Version.BindValueChanged(_ => { - pathTypes.Clear(); - - foreach (var point in slider.Path.ControlPoints) - { - IBindable boundTypeCopy = point.Type.GetBoundCopy(); - pathTypes.Add(boundTypeCopy); - boundTypeCopy.BindValueChanged(_ => PointsInSegment = slider.Path.PointsInSegment(controlPoint)); - } - - PointsInSegment = slider.Path.PointsInSegment(controlPoint); + PointsInSegment = slider.Path.PointsInSegment(ControlPoint); + updatePathType(); }, runOnceImmediately: true); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); @@ -120,9 +110,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.LoadComplete(); - sliderVersion = slider.Path.Version.GetBoundCopy(); - sliderVersion.BindValueChanged(_ => updatePathType()); - sliderPosition = slider.PositionBindable.GetBoundCopy(); sliderPosition.BindValueChanged(_ => updateMarkerDisplay()); From 8aff53172de5f153b5926926bc0bf1c8e75f3ef5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 15:17:53 +0900 Subject: [PATCH 60/92] Remove necessity for nested PassThroughInputManger --- .../Gameplay/TestSceneGameplayMenuOverlay.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 23 ++++++++++----- osu.Game/Input/Bindings/GlobalInputManager.cs | 29 ------------------- osu.Game/OsuGameBase.cs | 15 ++++++---- .../Visual/OsuManualInputManagerTestScene.cs | 2 +- 5 files changed, 26 insertions(+), 45 deletions(-) delete mode 100644 osu.Game/Input/Bindings/GlobalInputManager.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index 75e8194708..d69ac665cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay [BackgroundDependencyLoader] private void load(OsuGameBase game) { - Child = globalActionContainer = new GlobalActionContainer(game, null); + Child = globalActionContainer = new GlobalActionContainer(game); } [SetUp] diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 042960d54c..671c3bc8bc 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -13,20 +12,23 @@ namespace osu.Game.Input.Bindings { public class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput { - [CanBeNull] - private readonly GlobalInputManager globalInputManager; - private readonly Drawable handler; + private InputManager parentInputManager; - public GlobalActionContainer(OsuGameBase game, [CanBeNull] GlobalInputManager globalInputManager) + public GlobalActionContainer(OsuGameBase game) : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { - this.globalInputManager = globalInputManager; - if (game is IKeyBindingHandler) handler = game; } + protected override void LoadComplete() + { + base.LoadComplete(); + + parentInputManager = GetContainingInputManager(); + } + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings .Concat(EditorKeyBindings) .Concat(InGameKeyBindings) @@ -113,7 +115,12 @@ namespace osu.Game.Input.Bindings { get { - var inputQueue = globalInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue; + // To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content. + // It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly + // allow the whole game to handle these actions. + + // An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. + var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue; return handler != null ? inputQueue.Prepend(handler) : inputQueue; } diff --git a/osu.Game/Input/Bindings/GlobalInputManager.cs b/osu.Game/Input/Bindings/GlobalInputManager.cs deleted file mode 100644 index 91373712fb..0000000000 --- a/osu.Game/Input/Bindings/GlobalInputManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; - -namespace osu.Game.Input.Bindings -{ - public class GlobalInputManager : PassThroughInputManager - { - public readonly GlobalActionContainer GlobalBindings; - - protected override Container Content { get; } - - public GlobalInputManager(OsuGameBase game) - { - InternalChildren = new Drawable[] - { - Content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. - GlobalBindings = new GlobalActionContainer(game, this) - }; - } - } -} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 21313d0596..ca132df552 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -308,18 +308,21 @@ namespace osu.Game AddInternal(RulesetConfigCache); - var globalInput = new GlobalInputManager(this) + GlobalActionContainer globalBindings; + + var mainContent = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both } + MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }, + // to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. + globalBindings = new GlobalActionContainer(this) }; MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }; - base.Content.Add(CreateScalingContainer().WithChild(globalInput)); + base.Content.Add(CreateScalingContainer().WithChildren(mainContent)); - KeyBindingStore.Register(globalInput.GlobalBindings); - dependencies.Cache(globalInput.GlobalBindings); + KeyBindingStore.Register(globalBindings); + dependencies.Cache(globalBindings); PreviewTrackManager previewTrackManager; dependencies.Cache(previewTrackManager = new PreviewTrackManager()); diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 7dad636da7..01dd7a25c8 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual if (CreateNestedActionContainer) { - mainContent = new GlobalActionContainer(null, null).WithChild(mainContent); + mainContent = new GlobalActionContainer(null).WithChild(mainContent); } base.Content.AddRange(new Drawable[] From 545156d15ca19a67bc22c43aa6dded4cee3cd877 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 15:20:53 +0900 Subject: [PATCH 61/92] Add regression test coverage --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 00f9bf3432..859cefe3a9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -44,6 +44,20 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + /// + /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding + /// but should be handled *after* song select). + /// + [Test] + public void TestOpenModSelectOverlayUsingAction() + { + TestSongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestSongSelect()); + AddStep("Show mods overlay", () => InputManager.Key(Key.F1)); + AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + } + [Test] public void TestRetryCountIncrements() { From b73860cb5f6de153df50f3dc64b067dd611a6f40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 15:47:55 +0900 Subject: [PATCH 62/92] Slightly alter button colour scheme to make text more legible and reduce saturation --- .../Multiplayer/Match/MultiplayerSpectateButton.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 50be7719d9..4b3fb5d00f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -68,16 +68,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { default: button.Text = "Spectate"; - button.BackgroundColour = colours.Blue; - button.Triangles.ColourDark = colours.Blue; - button.Triangles.ColourLight = colours.BlueLight; + button.BackgroundColour = colours.BlueDark; + button.Triangles.ColourDark = colours.BlueDarker; + button.Triangles.ColourLight = colours.Blue; break; case MultiplayerUserState.Spectating: button.Text = "Stop spectating"; - button.BackgroundColour = colours.Red; - button.Triangles.ColourDark = colours.Red; - button.Triangles.ColourLight = colours.RedLight; + button.BackgroundColour = colours.Gray4; + button.Triangles.ColourDark = colours.Gray5; + button.Triangles.ColourLight = colours.Gray6; break; } From a55e62188ebe87648bcc1ffe9f6af93f167072a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 15:54:58 +0900 Subject: [PATCH 63/92] Change state icon to binoculars so the eye isn't staring at me --- .../Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index e6a407acec..2616b07c1f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants case MultiplayerUserState.Spectating: text.Text = "spectating"; - icon.Icon = FontAwesome.Solid.Eye; + icon.Icon = FontAwesome.Solid.Binoculars; icon.Colour = colours.BlueLight; break; From 725edfcbf35bdbdca7f1e277722ed7d749e5f8f2 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 09:05:35 +0200 Subject: [PATCH 64/92] Add path type menu change method --- .../Components/PathControlPointVisualiser.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ce5dc4855e..ff2e4ea152 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -153,6 +153,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + /// + /// Attempts to set the given control point piece to the given path type. + /// If that would fail, try to change the path such that it instead succeeds + /// in a UX-friendly way. + /// + /// The control point piece that we want to change the path type of. + /// The path type we want to assign to the given control point piece. + private void updatePathType(PathControlPointPiece piece, PathType? type) + { + piece.ControlPoint.Type.Value = type; + } + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -218,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components var item = new PathTypeMenuItem(type, () => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) - p.ControlPoint.Type.Value = type; + updatePathType(p, type); }); if (countOfState == totalCount) From 0341023d13cf7926a04d67852ec995874e10e79f Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 09:06:28 +0200 Subject: [PATCH 65/92] Improve UX of selecting PerfectCurve --- .../Components/PathControlPointVisualiser.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ff2e4ea152..0b163cbc44 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -162,6 +162,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// The path type we want to assign to the given control point piece. private void updatePathType(PathControlPointPiece piece, PathType? type) { + int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); + + switch (type) + { + case PathType.PerfectCurve: + if (piece.PointsInSegment.Count > 3) + { + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one bezier segment. + piece.PointsInSegment[indexInSegment + 2].Type.Value = PathType.Bezier; + } + + break; + } + piece.ControlPoint.Type.Value = type; } From b38d332268bf678cda1746bb264a7adb32d7a7e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 16:29:11 +0900 Subject: [PATCH 66/92] Fix broken test --- .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index dad1237991..dfb4306e67 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -209,9 +209,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { addClickButtonStep(); AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); - AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); + AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); AddStep("transitioned to gameplay", () => readyClickOperation.Dispose()); + + AddStep("finish gameplay", () => + { + Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded); + Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay); + }); + AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } } From fd2a14a0bf07fd5e8b940f2f666455fc4eb923ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 16:30:48 +0900 Subject: [PATCH 67/92] Only set button state once --- .../Multiplayer/Match/MultiplayerReadyButton.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 6919be2d56..f2dd9a6f25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -105,14 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; + bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; - // When the local user is the host and spectating the match, the "start match" state should be enabled. + // When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready. if (localUser.State == MultiplayerUserState.Spectating) - { - button.Enabled.Value &= Room?.Host?.Equals(localUser) == true; - button.Enabled.Value &= newCountReady > 0; - } + enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0; + + button.Enabled.Value = enableButton; if (newCountReady != countReady) { From be4520fe33ff6ec67367ef207a59a00fa47b8d61 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:46:00 +0200 Subject: [PATCH 68/92] Fix index out of range possibility --- .../Components/PathControlPointVisualiser.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 0b163cbc44..1dfbf97682 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -167,13 +167,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (type) { case PathType.PerfectCurve: - if (piece.PointsInSegment.Count > 3) - { - // Can't always create a circular arc out of 4 or more points, - // so we split the segment into one 3-point circular arc segment - // and one bezier segment. - piece.PointsInSegment[indexInSegment + 2].Type.Value = PathType.Bezier; - } + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one bezier segment. + int thirdPointIndex = indexInSegment + 2; + + if (piece.PointsInSegment.Count > thirdPointIndex + 1) + piece.PointsInSegment[thirdPointIndex].Type.Value = PathType.Bezier; break; } From 4110d1675d001919dca028a51f5e92b19eb5d611 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:46:52 +0200 Subject: [PATCH 69/92] Add path type menu test cases --- .../TestScenePathControlPointVisualiser.cs | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 738a21b17e..3e2d1cff0e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -4,9 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; @@ -14,7 +16,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Editor { - public class TestScenePathControlPointVisualiser : OsuTestScene + public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene { private Slider slider; private PathControlPointVisualiser visualiser; @@ -43,12 +45,107 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + [Test] + public void TestPerfectCurveTooManyPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.PerfectCurve); + AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); + } + + [Test] + public void TestPerfectCurveLastThreePoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(2); + AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(2, PathType.PerfectCurve); + assertControlPointPathType(4, null); + } + + [Test] + public void TestPerfectCurveLastTwoPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Bezier); + AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, Origin = Anchor.Centre }); - private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position))); + private void addControlPointStep(Vector2 position) => addControlPointStep(position, null); + + private void addControlPointStep(Vector2 position, PathType? type) + { + AddStep($"add {type} control point at {position}", () => + { + slider.Path.ControlPoints.Add(new PathControlPoint(position, type)); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + Vector2 position = slider.Path.ControlPoints[index].Position.Value; + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + }); + } + + private void assertControlPointPathType(int controlPointIndex, PathType? type) + { + AddAssert($"point {controlPointIndex} is {type}", () => + { + return slider.Path.ControlPoints[controlPointIndex].Type.Value == type; + }); + } + + private void addContextMenuItemStep(string contextMenuText) + { + AddStep($"click context menu item \"{contextMenuText}\"", () => + { + MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + + item?.Action?.Value(); + }); + } } } From 7d2b54ca42b5d98ffaf69b5bbaba2cbf5f8059df Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:32:45 +0200 Subject: [PATCH 70/92] Add change to Bezier test --- .../TestScenePathControlPointVisualiser.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 3e2d1cff0e..1d5a175a26 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -105,6 +105,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); } + [Test] + public void TestPerfectCurveChangeToBezier() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(300), PathType.PerfectCurve); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200), PathType.Bezier); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(3); + AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); + addContextMenuItemStep("Inherit"); + + assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(1, PathType.Bezier); + assertControlPointPathType(3, null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, From 9a675a22195fe0d24576dbffec32285282f3209c Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:33:43 +0200 Subject: [PATCH 71/92] Correct 4+ point perfect curves to Bezier --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 20d4fe4ea9..6b78cff33e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -213,10 +213,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (ControlPoint.Type.Value != PathType.PerfectCurve) return; - ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); - if (points.Length != 3) + if (PointsInSegment.Count > 3) + ControlPoint.Type.Value = PathType.Bezier; + + if (PointsInSegment.Count != 3) return; + ReadOnlySpan points = PointsInSegment.Select(p => p.Position.Value).ToArray(); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); if (boundingBox.Width >= 640 || boundingBox.Height >= 480) ControlPoint.Type.Value = PathType.Bezier; From 2d9448456671c8b7e1966341af66338f45923f85 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:49:46 +0200 Subject: [PATCH 72/92] Use lambda expression Apparently CI dislikes this not being a lambda. --- .../Editor/TestScenePathControlPointVisualiser.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 1d5a175a26..440ab7c889 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -152,10 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertControlPointPathType(int controlPointIndex, PathType? type) { - AddAssert($"point {controlPointIndex} is {type}", () => - { - return slider.Path.ControlPoints[controlPointIndex].Type.Value == type; - }); + AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type); } private void addContextMenuItemStep(string contextMenuText) From 7713c8a45f0763a7aff7b24622e58a203c7d1c4b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 20:19:41 +0900 Subject: [PATCH 73/92] Add support for sliderwhistle --- .../Objects/Drawables/DrawableSlider.cs | 10 +++++++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 04708a5ece..8167a9f243 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osuTK; @@ -110,13 +111,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - var firstSample = HitObject.Samples.FirstOrDefault(); + var firstSample = HitObject.OriginalSamples.FirstOrDefault(); if (firstSample != null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); + var samples = new List { HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide") }; - slidingSample.Samples = new ISampleInfo[] { clone }; + if (HitObject.OriginalSamples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) + samples.Add(HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderwhistle")); + + slidingSample.Samples = samples.ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e2b6c84896..54e781bbe9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); + public IList OriginalSamples { get; private set; } + private int repeatCount; public int RepeatCount @@ -147,6 +149,7 @@ namespace osu.Game.Rulesets.Osu.Objects // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. // For now, the samples are attached to and played by the slider itself at the correct end time. // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). + OriginalSamples = Samples.ToList(); Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } From 7d291ed7d73f18bcb9bdac5f1aa26f511ee6ecf6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 20:57:50 +0900 Subject: [PATCH 74/92] Don't serialise OriginalSamples --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 54e781bbe9..4e71d82cab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -81,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); + [JsonIgnore] public IList OriginalSamples { get; private set; } private int repeatCount; From 70cd018a984cf2c80e354b367ffe5698e6dda39b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 21:38:58 +0900 Subject: [PATCH 75/92] Fix intermittent test failure --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 6f8ec7fcfb..839118de2f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait", 10); + AddUntilStep("wait for join", () => Client.Room != null); } [Test] @@ -114,6 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for room join", () => Client.Room != null); + AddStep("join other user (ready)", () => { Client.AddUser(new User { Id = 55 }); From 8efa381d3a1f71eb8e169495bf5a54f99a4a751c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 8 Apr 2021 23:13:16 +0900 Subject: [PATCH 76/92] Actually use whistle sample for sliderwhistle --- .../Objects/Drawables/DrawableSlider.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 8167a9f243..e29e28b1fa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -111,17 +111,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - var firstSample = HitObject.OriginalSamples.FirstOrDefault(); + var slidingSamples = new List(); - if (firstSample != null) - { - var samples = new List { HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide") }; + var normalSample = HitObject.OriginalSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + if (normalSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide")); - if (HitObject.OriginalSamples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE)) - samples.Add(HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderwhistle")); + var whistleSample = HitObject.OriginalSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + if (whistleSample != null) + slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle")); - slidingSample.Samples = samples.ToArray(); - } + slidingSample.Samples = slidingSamples.ToArray(); } public override void StopAllSamples() From 24ae5b91696c8141588ba379081d075196fe7e6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 23:15:08 +0900 Subject: [PATCH 77/92] Fix slightly incorrect solo score submission routes --- osu.Game/Online/Solo/CreateSoloScoreRequest.cs | 2 +- osu.Game/Online/Solo/SubmitSoloScoreRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs index ae5ac5e26c..dea5a77496 100644 --- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs @@ -27,6 +27,6 @@ namespace osu.Game.Online.Solo return req; } - protected override string Target => $@"solo/{beatmapId}/scores"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores"; } } diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs index 98ba4fa052..85fa3eeb34 100644 --- a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs @@ -40,6 +40,6 @@ namespace osu.Game.Online.Solo return req; } - protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}"; + protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}"; } } From 5dd6f19ec5b74b9f0b68802562ad858b8b374434 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Apr 2021 23:15:34 +0900 Subject: [PATCH 78/92] Update rider metadata files for 2021.1 --- .idea/.idea.osu.Desktop/.idea/indexLayout.xml | 2 +- .idea/.idea.osu.Desktop/.idea/modules.xml | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) delete mode 100644 .idea/.idea.osu.Desktop/.idea/modules.xml diff --git a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml index 27ba142e96..7b08163ceb 100644 --- a/.idea/.idea.osu.Desktop/.idea/indexLayout.xml +++ b/.idea/.idea.osu.Desktop/.idea/indexLayout.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml deleted file mode 100644 index 680312ad27..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file From 8293b06c0afe9dfe16e1340f9ffab8ca1e737fd5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 13:56:55 +0900 Subject: [PATCH 79/92] Remove obsolete code --- osu.Game/Beatmaps/BeatmapConverter.cs | 34 +------------------ osu.Game/Beatmaps/BeatmapStatistic.cs | 12 ------- osu.Game/Overlays/Settings/SettingsItem.cs | 7 ---- osu.Game/Rulesets/Judgements/Judgement.cs | 12 ------- .../Objects/Drawables/DrawableHitObject.cs | 18 ---------- osu.Game/Rulesets/Objects/HitObject.cs | 9 ----- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 6 ---- 7 files changed, 1 insertion(+), 97 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b291edd19d..f3434c5153 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -27,8 +27,6 @@ namespace osu.Game.Beatmaps public IBeatmap Beatmap { get; } - private CancellationToken cancellationToken; - protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; @@ -41,8 +39,6 @@ namespace osu.Game.Beatmaps public IBeatmap Convert(CancellationToken cancellationToken = default) { - this.cancellationToken = cancellationToken; - // We always operate on a clone of the original beatmap, to not modify it game-wide return ConvertBeatmap(Beatmap.Clone(), cancellationToken); } @@ -55,19 +51,6 @@ namespace osu.Game.Beatmaps /// The converted Beatmap. protected virtual Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { -#pragma warning disable 618 - return ConvertBeatmap(original); -#pragma warning restore 618 - } - - /// - /// Performs the conversion of a Beatmap using this Beatmap Converter. - /// - /// The un-converted Beatmap. - /// The converted Beatmap. - [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 - protected virtual Beatmap ConvertBeatmap(IBeatmap original) - { var beatmap = CreateBeatmap(); beatmap.BeatmapInfo = original.BeatmapInfo; @@ -121,21 +104,6 @@ namespace osu.Game.Beatmaps /// The un-converted Beatmap. /// The cancellation token. /// The converted hit object. - protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) - { -#pragma warning disable 618 - return ConvertHitObject(original, beatmap); -#pragma warning restore 618 - } - - /// - /// Performs the conversion of a hit object. - /// This method is generally executed sequentially for all objects in a beatmap. - /// - /// The hit object to convert. - /// The un-converted Beatmap. - /// The converted hit object. - [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 - protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty(); + protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => Enumerable.Empty(); } } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 9d87a20d60..7d7ba09fcf 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -3,16 +3,11 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osuTK; namespace osu.Game.Beatmaps { public class BeatmapStatistic { - [Obsolete("Use CreateIcon instead")] // can be removed 20210203 - public IconUsage Icon = FontAwesome.Regular.QuestionCircle; - /// /// A function to create the icon for display purposes. Use default icons available via whenever possible for conformity. /// @@ -20,12 +15,5 @@ namespace osu.Game.Beatmaps public string Content; public string Name; - - public BeatmapStatistic() - { -#pragma warning disable 618 - CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) }; -#pragma warning restore 618 - } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 85765bf991..0bd9750b0b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -57,13 +57,6 @@ namespace osu.Game.Overlays.Settings } } - [Obsolete("Use Current instead")] // Can be removed 20210406 - public Bindable Bindable - { - get => Current; - set => Current = value; - } - public virtual Bindable Current { get => controlWithCurrent.Current; diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index b1ca72b1c0..be69db5ca8 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -28,18 +28,6 @@ namespace osu.Game.Rulesets.Judgements /// protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; - /// - /// Whether this should affect the current combo. - /// - [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 - public virtual bool AffectsCombo => true; - - /// - /// Whether this should be counted as base (combo) or bonus score. - /// - [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 - public virtual bool IsBonus => !AffectsCombo; - /// /// The maximum that can be achieved. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e5eaf5db88..e69c4e2d91 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -736,24 +736,6 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); - // Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements. - // Can be removed 20210328 - if (Result.Judgement.MaxResult == HitResult.IgnoreHit) - { - HitResult originalType = Result.Type; - - if (Result.Type == HitResult.Miss) - Result.Type = HitResult.IgnoreMiss; - else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect) - Result.Type = HitResult.IgnoreHit; - - if (Result.Type != originalType) - { - Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n" - + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2021-03-28 onwards.", level: LogLevel.Important); - } - } - if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) { throw new InvalidOperationException( diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 826d411822..db02eafa92 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -139,15 +139,6 @@ namespace osu.Game.Rulesets.Objects } protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken) - { - // ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520) -#pragma warning disable 618 - CreateNestedHitObjects(); -#pragma warning restore 618 - } - - [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 - protected virtual void CreateNestedHitObjects() { } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b81fa79345..0fb5c2f4b5 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -346,12 +346,6 @@ namespace osu.Game.Rulesets.Scoring score.HitEvents = hitEvents; } - - /// - /// Create a for this processor. - /// - [Obsolete("Method is now unused.")] // Can be removed 20210328 - public virtual HitWindows CreateHitWindows() => new HitWindows(); } public enum ScoringMode From 76981f25478029cf948324f9e4c2e9e780ce57e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 13:58:24 +0900 Subject: [PATCH 80/92] Remove unused using --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e69c4e2d91..6da9f12b50 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; From 51fee79ef19946f48262bd14fce5c7bc5254bcdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Apr 2021 15:18:02 +0900 Subject: [PATCH 81/92] Fix scores not being accepted due to missing ruleset ID --- osu.Game/Online/Solo/CreateSoloScoreRequest.cs | 6 +++++- osu.Game/Screens/Play/SoloPlayer.cs | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs index dea5a77496..0d2bea1f2a 100644 --- a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs +++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.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.Globalization; using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; @@ -11,11 +12,13 @@ namespace osu.Game.Online.Solo public class CreateSoloScoreRequest : APIRequest { private readonly int beatmapId; + private readonly int rulesetId; private readonly string versionHash; - public CreateSoloScoreRequest(int beatmapId, string versionHash) + public CreateSoloScoreRequest(int beatmapId, int rulesetId, string versionHash) { this.beatmapId = beatmapId; + this.rulesetId = rulesetId; this.versionHash = versionHash; } @@ -24,6 +27,7 @@ namespace osu.Game.Online.Solo var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; req.AddParameter("version_hash", versionHash); + req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture)); return req; } diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index ee1ccdc5b3..d0ef4131dc 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -17,7 +17,10 @@ namespace osu.Game.Screens.Play if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId)) return null; - return new CreateSoloScoreRequest(beatmapId, Game.VersionHash); + if (!(Ruleset.Value.ID is int rulesetId)) + return null; + + return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash); } protected override bool HandleTokenRetrievalFailure(Exception exception) => false; From f2e811928bb30d2b4c5a69bfcce67f5dbac7a68e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 15:28:08 +0900 Subject: [PATCH 82/92] Rework slider hackery to not overwrite Samples --- .../Objects/Drawables/DrawableSlider.cs | 14 +++++++++++--- osu.Game.Rulesets.Osu/Objects/Slider.cs | 12 +++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e29e28b1fa..82b5492de6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -109,15 +109,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void LoadSamples() { - base.LoadSamples(); + // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. + + if (HitObject.SampleControlPoint == null) + { + throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); + } + + Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); var slidingSamples = new List(); - var normalSample = HitObject.OriginalSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); + var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL); if (normalSample != null) slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide")); - var whistleSample = HitObject.OriginalSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); + var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE); if (whistleSample != null) slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle")); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4e71d82cab..8ba9597dc3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects public List> NodeSamples { get; set; } = new List>(); [JsonIgnore] - public IList OriginalSamples { get; private set; } + public IList TailSamples { get; private set; } private int repeatCount; @@ -146,12 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; - - // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. - // For now, the samples are attached to and played by the slider itself at the correct end time. - // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). - OriginalSamples = Samples.ToList(); - Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) @@ -242,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects if (HeadCircle != null) HeadCircle.Samples = this.GetNodeSamples(0); + + // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // For now, the samples are played by the slider itself at the correct end time. + TailSamples = this.GetNodeSamples(repeatCount + 1); } public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); From 9b0ce2999ffe05292826c048942058da5d1679d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Apr 2021 15:28:42 +0900 Subject: [PATCH 83/92] Fix legacy encoder --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index da44b96ed3..acbf57d25f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -273,7 +273,7 @@ namespace osu.Game.Beatmaps.Formats if (hitObject is IHasPath path) { addPathData(writer, path, position); - writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); + writer.Write(getSampleBank(hitObject.Samples)); } else { @@ -420,15 +420,15 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}")); } - private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) + private string getSampleBank(IList samples, bool banksOnly = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); StringBuilder sb = new StringBuilder(); - sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:")); - sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}")); + sb.Append(FormattableString.Invariant($"{(int)normalBank}:")); + sb.Append(FormattableString.Invariant($"{(int)addBank}")); if (!banksOnly) { From b10ee7482d8b72986448b2c33ec81d898df16556 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 25 Dec 2020 15:43:50 +0900 Subject: [PATCH 84/92] Add a failing test to check catch replay accuracy --- .../TestSceneCatchReplay.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs new file mode 100644 index 0000000000..a10371b0f7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneCatchReplay : TestSceneCatchPlayer + { + protected override bool Autoplay => true; + + private const int object_count = 10; + + [Test] + public void TestReplayCatcherPositionIsFramePerfect() + { + AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new Beatmap + { + BeatmapInfo = + { + Ruleset = ruleset, + } + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + + for (int i = 0; i < object_count / 2; i++) + { + beatmap.HitObjects.Add(new Fruit + { + StartTime = (i + 1) * 1000, + X = 0 + }); + beatmap.HitObjects.Add(new Fruit + { + StartTime = (i + 1) * 1000 + 1, + X = CatchPlayfield.WIDTH + }); + } + + return beatmap; + } + } +} From 6d0dc62502ecd82dd409b85ffcfdb6bee1a61f2a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 9 Apr 2021 16:04:45 +0900 Subject: [PATCH 85/92] Make sure latest catcher position is used for catching logic A replay frame processed in CatchInputManager is applied to catcher in `CatcherArea`. The catcher position is then used for the catching logic for each hit object under `HitObjectContainer`. Thus, if `HitObjectContainer` came before `CatcherArea`, the replay input is delayed one frame. That was one reason why the catch autoplay misses hit objects (especially when fast-forwarded). --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 73420a9eda..0e1ef90737 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -51,8 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI { droppedObjectContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), - HitObjectContainer, + HitObjectContainer.CreateProxy(), + // This ordering (`CatcherArea` before `HitObjectContainer`) is important to + // make sure the up-to-date catcher position is used for the catcher catching logic of hit objects. CatcherArea, + HitObjectContainer, }; } From 0af6d77192e3af39430098a2dc35c26cdde55d69 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Fri, 9 Apr 2021 11:03:38 +0200 Subject: [PATCH 86/92] Test for path type transfer --- .../TestScenePathControlPointVisualiser.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 440ab7c889..35b79aa8ac 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPathType(0, PathType.Bezier); assertControlPointPathType(1, PathType.PerfectCurve); - AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); + assertControlPointPathType(3, PathType.Bezier); } [Test] @@ -105,6 +105,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); } + [Test] + public void TestPerfectCurveTooManyPointsLinear() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.Linear); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + // Must be both hovering and selecting the control point for the context menu to work. + moveMouseToControlPoint(1); + AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); + addContextMenuItemStep("Perfect curve"); + + assertControlPointPathType(0, PathType.Linear); + assertControlPointPathType(1, PathType.PerfectCurve); + assertControlPointPathType(3, PathType.Linear); + } + [Test] public void TestPerfectCurveChangeToBezier() { From f64b2095bf6c06de31b2cc0ca7d9b888dc101347 Mon Sep 17 00:00:00 2001 From: Naxess <30292137+Naxesss@users.noreply.github.com> Date: Fri, 9 Apr 2021 11:04:00 +0200 Subject: [PATCH 87/92] Carry over the previous path type --- .../Sliders/Components/PathControlPointVisualiser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 1dfbf97682..44c3056910 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -169,11 +169,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case PathType.PerfectCurve: // Can't always create a circular arc out of 4 or more points, // so we split the segment into one 3-point circular arc segment - // and one bezier segment. + // and one segment of the previous type. int thirdPointIndex = indexInSegment + 2; if (piece.PointsInSegment.Count > thirdPointIndex + 1) - piece.PointsInSegment[thirdPointIndex].Type.Value = PathType.Bezier; + piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value; break; } From 8a0da06e89eed9d93c32db7b045e6bd3c021853b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 9 Apr 2021 22:40:21 +0900 Subject: [PATCH 88/92] Add a hover sample type for SongSelect buttons --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 5 ++++- osu.Game/Screens/Select/FooterButton.cs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 00cf5798e7..49bcc3759b 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -53,6 +53,9 @@ namespace osu.Game.Graphics.UserInterface Soft, [Description("-toolbar")] - Toolbar + Toolbar, + + [Description("-songselect")] + SongSelect } } diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 7528651fd9..afb3943a09 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Select { @@ -65,6 +66,7 @@ namespace osu.Game.Screens.Select private readonly Box light; public FooterButton() + : base(HoverSampleSet.SongSelect) { AutoSizeAxes = Axes.Both; Shear = SHEAR; From ffacd38e573bb0f7b01a531bfde6c1ca9383ac96 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 9 Apr 2021 22:41:51 +0900 Subject: [PATCH 89/92] Reduce the randomised pitch range of hover sounds --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselHeader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 00cf5798e7..4ba79236d9 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface public override void PlayHoverSample() { - sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + sampleHover.Frequency.Value = 0.98 + RNG.NextDouble(0.04); sampleHover.Play(); } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 2fbf64de29..40ca3e0764 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Select.Carousel { if (sampleHover == null) return; - sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); + sampleHover.Frequency.Value = 0.99 + RNG.NextDouble(0.02); sampleHover.Play(); } } From bfd3d0cce978c77d4bfa7bbe1a15262e4cc23c80 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 10 Apr 2021 01:16:54 +1000 Subject: [PATCH 90/92] Implement custom enumerator for ReverseQueue to avoid allocations --- .../Rulesets/Difficulty/Utils/ReverseQueue.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs index f104b8105a..57db9df3ca 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs @@ -101,12 +101,33 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// /// Returns an enumerator which enumerates items in the starting from the most recently enqueued item. /// - public IEnumerator GetEnumerator() - { - for (int i = Count - 1; i >= 0; i--) - yield return items[(start + i) % capacity]; - } + public IEnumerator GetEnumerator() => new Enumerator(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private ReverseQueue reverseQueue; + private int currentIndex; + + internal Enumerator(ReverseQueue reverseQueue) + { + this.reverseQueue = reverseQueue; + currentIndex = -1; // The first MoveNext() should bring the iterator to 0 + } + + public bool MoveNext() => ++currentIndex < reverseQueue.Count; + + public void Reset() => currentIndex = -1; + + public readonly T Current => reverseQueue[currentIndex]; + + readonly object IEnumerator.Current => Current; + + public void Dispose() + { + reverseQueue = null; + } + } } } From affc878db9ff3a0e322dc5d25c8bfcbb76d98a33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Apr 2021 01:03:15 +0900 Subject: [PATCH 91/92] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index cba3975209..31308b80d8 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 292e5b932f..16b888a064 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 36e581a80c..f72a1eb256 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From b66ef2fdec193cc0227e974c56de92937aabf73b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Apr 2021 02:14:28 +0900 Subject: [PATCH 92/92] 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 31308b80d8..196d122a2a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 16b888a064..71a6f0e5cd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f72a1eb256..a389cc13dd 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +