From cc4c5f72d893bc014d4172b1e2ba1c0930165aa4 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Sat, 20 Feb 2021 21:48:31 +0100 Subject: [PATCH 01/39] Move logic to keep selection in bounds into it's own method --- .../Edit/OsuSelectionHandler.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 871339ae7b..51e0e80e30 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -211,26 +211,35 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - Quad quad = getSurroundingQuad(hitObjects); - - Vector2 newTopLeft = quad.TopLeft + delta; - if (newTopLeft.X < 0) - delta.X -= newTopLeft.X; - if (newTopLeft.Y < 0) - delta.Y -= newTopLeft.Y; - - Vector2 newBottomRight = quad.BottomRight + delta; - if (newBottomRight.X > DrawWidth) - delta.X -= newBottomRight.X - DrawWidth; - if (newBottomRight.Y > DrawHeight) - delta.Y -= newBottomRight.Y - DrawHeight; - foreach (var h in hitObjects) h.Position += delta; + moveSelectionInBounds(); + return true; } + private void moveSelectionInBounds() + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + Vector2 delta = new Vector2(0); + + if (quad.TopLeft.X < 0) + delta.X -= quad.TopLeft.X; + if (quad.TopLeft.Y < 0) + delta.Y -= quad.TopLeft.Y; + + if (quad.BottomRight.X > DrawWidth) + delta.X -= quad.BottomRight.X - DrawWidth; + if (quad.BottomRight.Y > DrawHeight) + delta.Y -= quad.BottomRight.Y - DrawHeight; + + foreach (var h in hitObjects) + h.Position += delta; + } + /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// From 0b8009938a4c6c84b406cf6fd15ccd76e150935f Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Sat, 20 Feb 2021 21:50:30 +0100 Subject: [PATCH 02/39] Prevent selection from breaking playfield bounds when scaling --- .../Edit/OsuSelectionHandler.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 51e0e80e30..6c62bdd922 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -135,14 +135,21 @@ namespace osu.Game.Rulesets.Osu.Edit adjustScaleFromAnchor(ref scale, reference); var hitObjects = selectedMovableObjects; + Quad selectionQuad = getSurroundingQuad(hitObjects); + + float newWidth = selectionQuad.Width + scale.X; + float newHeight = selectionQuad.Height + scale.Y; + + if ((newHeight > DrawHeight) || (newWidth > DrawWidth)) + return false; // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) { - 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); + Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); foreach (var point in slider.Path.ControlPoints) point.Position.Value *= pathRelativeDeltaScale; @@ -153,23 +160,23 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; - Quad quad = getSurroundingQuad(hitObjects); - foreach (var h in hitObjects) { var newPosition = h.Position; // guard against no-ops and NaN. - if (scale.X != 0 && quad.Width > 0) - newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X); + if (scale.X != 0 && selectionQuad.Width > 0) + newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - if (scale.Y != 0 && quad.Height > 0) - newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y); + if (scale.Y != 0 && selectionQuad.Height > 0) + newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); h.Position = newPosition; } } + moveSelectionInBounds(); + return true; } From 562a4cefdb952b54d3b6c20eda47e13b78378010 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Sun, 21 Feb 2021 12:12:32 +0100 Subject: [PATCH 03/39] Simplify HandleScale by extracting methods --- .../Edit/OsuSelectionHandler.cs | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 6c62bdd922..9418752745 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -143,41 +143,18 @@ namespace osu.Game.Rulesets.Osu.Edit if ((newHeight > DrawHeight) || (newWidth > DrawWidth)) return false; + bool result; // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) - { - Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); - Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); - - foreach (var point in slider.Path.ControlPoints) - point.Position.Value *= pathRelativeDeltaScale; - } + result = scaleSlider(slider, scale); else - { - // move the selection before scaling if dragging from top or left anchors. - if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; - if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; - - foreach (var h in hitObjects) - { - var newPosition = h.Position; - - // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0) - newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - - if (scale.Y != 0 && selectionQuad.Height > 0) - newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); - - h.Position = newPosition; - } - } + result = scaleHitObjects(hitObjects, reference, scale); moveSelectionInBounds(); - return true; + return result; } private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) @@ -214,6 +191,42 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + private bool scaleSlider(Slider slider, Vector2 scale) + { + 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; + + return true; + } + + private bool scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) + { + // move the selection before scaling if dragging from top or left anchors. + if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; + if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; + + Quad selectionQuad = getSurroundingQuad(hitObjects); + + foreach (var h in hitObjects) + { + var newPosition = h.Position; + + // guard against no-ops and NaN. + if (scale.X != 0 && selectionQuad.Width > 0) + newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + + if (scale.Y != 0 && selectionQuad.Height > 0) + newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + + h.Position = newPosition; + } + + return true; + } + private bool moveSelection(Vector2 delta) { var hitObjects = selectedMovableObjects; From 2c6f92d12fb2d2a55ad8bb9f28e4f32634aadf8b Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Sun, 21 Feb 2021 17:38:50 +0100 Subject: [PATCH 04/39] Move bounds check from moveSelection to HandleMovement --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9418752745..9ca8404a26 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -35,8 +35,12 @@ namespace osu.Game.Rulesets.Osu.Edit referenceOrigin = null; } - public override bool HandleMovement(MoveSelectionEvent moveEvent) => - moveSelection(moveEvent.InstantDelta); + public override bool HandleMovement(MoveSelectionEvent moveEvent) + { + bool result = moveSelection(moveEvent.InstantDelta); + moveSelectionInBounds(); + return result; + } /// /// During a transform, the initial origin is stored so it can be used throughout the operation. @@ -234,8 +238,6 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var h in hitObjects) h.Position += delta; - moveSelectionInBounds(); - return true; } From 33985d9e7c27a3d2ec58e5a2cd6f3068e50be7ab Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Sun, 21 Feb 2021 17:40:57 +0100 Subject: [PATCH 05/39] Rewrite scaling bounds check to behave more intuively --- .../Edit/OsuSelectionHandler.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9ca8404a26..64dbe20c58 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -139,15 +139,8 @@ namespace osu.Game.Rulesets.Osu.Edit adjustScaleFromAnchor(ref scale, reference); var hitObjects = selectedMovableObjects; - Quad selectionQuad = getSurroundingQuad(hitObjects); - - float newWidth = selectionQuad.Width + scale.X; - float newHeight = selectionQuad.Height + scale.Y; - - if ((newHeight > DrawHeight) || (newWidth > DrawWidth)) - return false; - bool result; + // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. @@ -197,8 +190,18 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleSlider(Slider slider, Vector2 scale) { - 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); + Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); + + Quad selectionQuad = getSurroundingQuad(new OsuHitObject[] { slider }); + Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); + (bool X, bool Y) inBounds = isQuadInBounds(scaledQuad); + + if (!inBounds.X) + pathRelativeDeltaScale.X = 1; + + if (!inBounds.Y) + pathRelativeDeltaScale.Y = 1; foreach (var point in slider.Path.ControlPoints) point.Position.Value *= pathRelativeDeltaScale; @@ -213,16 +216,18 @@ namespace osu.Game.Rulesets.Osu.Edit if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; Quad selectionQuad = getSurroundingQuad(hitObjects); + Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); + (bool X, bool Y) inBounds = isQuadInBounds(scaledQuad); foreach (var h in hitObjects) { var newPosition = h.Position; // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0) + if (scale.X != 0 && selectionQuad.Width > 0 && inBounds.X) newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - if (scale.Y != 0 && selectionQuad.Height > 0) + if (scale.Y != 0 && selectionQuad.Height > 0 && inBounds.Y) newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); h.Position = newPosition; @@ -231,6 +236,16 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + private (bool X, bool Y) isQuadInBounds(Quad quad) + { + (bool X, bool Y) result; + + result.X = (quad.TopLeft.X >= 0) && (quad.BottomRight.X < DrawWidth); + result.Y = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y < DrawHeight); + + return result; + } + private bool moveSelection(Vector2 delta) { var hitObjects = selectedMovableObjects; From 3491021f72a00eb5f4471dd6828678d2e6017315 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Feb 2021 20:58:46 +0100 Subject: [PATCH 06/39] Move moveSelection into HandleMovement --- .../Edit/OsuSelectionHandler.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 64dbe20c58..03c1676982 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -37,9 +37,13 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleMovement(MoveSelectionEvent moveEvent) { - bool result = moveSelection(moveEvent.InstantDelta); + var hitObjects = selectedMovableObjects; + + foreach (var h in hitObjects) + h.Position += moveEvent.InstantDelta; + moveSelectionInBounds(); - return result; + return true; } /// @@ -246,16 +250,6 @@ namespace osu.Game.Rulesets.Osu.Edit return result; } - private bool moveSelection(Vector2 delta) - { - var hitObjects = selectedMovableObjects; - - foreach (var h in hitObjects) - h.Position += delta; - - return true; - } - private void moveSelectionInBounds() { var hitObjects = selectedMovableObjects; From 71b30bdbbb7de4988ca31afe8110a477f98ab4b2 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Feb 2021 00:16:35 +0100 Subject: [PATCH 07/39] Adjust tuple usage --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 03c1676982..e1ee0612fa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -199,12 +199,12 @@ namespace osu.Game.Rulesets.Osu.Edit Quad selectionQuad = getSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); - (bool X, bool Y) inBounds = isQuadInBounds(scaledQuad); + (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - if (!inBounds.X) + if (!xInBounds) pathRelativeDeltaScale.X = 1; - if (!inBounds.Y) + if (!yInBounds) pathRelativeDeltaScale.Y = 1; foreach (var point in slider.Path.ControlPoints) @@ -221,17 +221,17 @@ namespace osu.Game.Rulesets.Osu.Edit Quad selectionQuad = getSurroundingQuad(hitObjects); Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); - (bool X, bool Y) inBounds = isQuadInBounds(scaledQuad); + (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); foreach (var h in hitObjects) { var newPosition = h.Position; // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0 && inBounds.X) + if (scale.X != 0 && selectionQuad.Width > 0 && xInBounds) newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - if (scale.Y != 0 && selectionQuad.Height > 0 && inBounds.Y) + if (scale.Y != 0 && selectionQuad.Height > 0 && yInBounds) newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); h.Position = newPosition; From 2a4139a2070df2f324526c651f9844fa58be1288 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Feb 2021 00:25:40 +0100 Subject: [PATCH 08/39] Refactor isQuadInBounds --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e1ee0612fa..ede756ab47 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -242,12 +242,10 @@ namespace osu.Game.Rulesets.Osu.Edit private (bool X, bool Y) isQuadInBounds(Quad quad) { - (bool X, bool Y) result; + bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X < DrawWidth); + bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y < DrawHeight); - result.X = (quad.TopLeft.X >= 0) && (quad.BottomRight.X < DrawWidth); - result.Y = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y < DrawHeight); - - return result; + return (xInBounds, yInBounds); } private void moveSelectionInBounds() From 877e19421bfd44426bfcb15956fd75cc2dfabea0 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Feb 2021 16:36:51 +0100 Subject: [PATCH 09/39] Refactor movement while scaling --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index ede756ab47..28e6347f4f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -216,11 +216,11 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { // move the selection before scaling if dragging from top or left anchors. - if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; - if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; Quad selectionQuad = getSurroundingQuad(hitObjects); - Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); + Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); foreach (var h in hitObjects) @@ -229,10 +229,10 @@ namespace osu.Game.Rulesets.Osu.Edit // guard against no-ops and NaN. if (scale.X != 0 && selectionQuad.Width > 0 && xInBounds) - newPosition.X = selectionQuad.TopLeft.X + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); + newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); if (scale.Y != 0 && selectionQuad.Height > 0 && yInBounds) - newPosition.Y = selectionQuad.TopLeft.Y + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); + newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); h.Position = newPosition; } From def0e5c42e90e5e8eb984c794a31eee0567caee4 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Mar 2021 17:21:42 +0100 Subject: [PATCH 10/39] Fix off-by-one error in isQuadInBounds --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 28e6347f4f..0418aa1925 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -242,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Edit private (bool X, bool Y) isQuadInBounds(Quad quad) { - bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X < DrawWidth); - bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y < DrawHeight); + bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth); + bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight); return (xInBounds, yInBounds); } From 3d471d239f2007c05e7af3bf417bae1c89b1d910 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Mar 2021 12:41:43 +0100 Subject: [PATCH 11/39] Clamp multi-object scale instead of cancelling it --- .../Edit/OsuSelectionHandler.cs | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 0418aa1925..595357ee65 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -215,23 +215,23 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { + scale = getClampedScale(hitObjects, reference, scale); + // move the selection before scaling if dragging from top or left anchors. float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; Quad selectionQuad = getSurroundingQuad(hitObjects); - Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); - (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); foreach (var h in hitObjects) { var newPosition = h.Position; // guard against no-ops and NaN. - if (scale.X != 0 && selectionQuad.Width > 0 && xInBounds) + if (scale.X != 0 && selectionQuad.Width > 0) newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X); - if (scale.Y != 0 && selectionQuad.Height > 0 && yInBounds) + if (scale.Y != 0 && selectionQuad.Height > 0) newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y); h.Position = newPosition; @@ -269,6 +269,43 @@ namespace osu.Game.Rulesets.Osu.Edit h.Position += delta; } + /// + /// Clamp scale where selection does not exceed playfield bounds or flip. + /// + /// The hitobjects to be scaled + /// The anchor from which the scale operation is performed + /// The scale to be clamped + /// The clamped scale vector + private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) + { + float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0; + float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0; + + Quad selectionQuad = getSurroundingQuad(hitObjects); + + //todo: this is not always correct for selections involving sliders + Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); + + //max Size -> playfield bounds + if (scaledQuad.TopLeft.X < 0) + scale.X += scaledQuad.TopLeft.X; + if (scaledQuad.TopLeft.Y < 0) + scale.Y += scaledQuad.TopLeft.Y; + + if (scaledQuad.BottomRight.X > DrawWidth) + scale.X -= scaledQuad.BottomRight.X - DrawWidth; + if (scaledQuad.BottomRight.Y > DrawHeight) + scale.Y -= scaledQuad.BottomRight.Y - DrawHeight; + + //min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale. + Vector2 scaledSize = selectionQuad.Size + scale; + Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON); + + scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size; + + return scale; + } + /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// From e67ab3cca7cb55383a82769499686a37e09f82e6 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 23 Mar 2021 16:09:44 +0100 Subject: [PATCH 12/39] Change single slider scaling to a method that works --- .../Edit/OsuSelectionHandler.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 595357ee65..ae92b12fd2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -197,19 +197,19 @@ namespace osu.Game.Rulesets.Osu.Edit Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); - Quad selectionQuad = getSurroundingQuad(new OsuHitObject[] { slider }); - Quad scaledQuad = new Quad(selectionQuad.TopLeft.X, selectionQuad.TopLeft.Y, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); - (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); - - if (!xInBounds) - pathRelativeDeltaScale.X = 1; - - if (!yInBounds) - pathRelativeDeltaScale.Y = 1; - foreach (var point in slider.Path.ControlPoints) point.Position.Value *= pathRelativeDeltaScale; + //if sliderhead or sliderend end up outside playfield, revert scaling. + Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); + (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); + + if (xInBounds && yInBounds) + return true; + + foreach (var point in slider.Path.ControlPoints) + point.Position.Value *= new Vector2(1 / pathRelativeDeltaScale.X, 1 / pathRelativeDeltaScale.Y); + return true; } From 013ddc734c09c110b9590b71cd1f04aa1a5bf021 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Mar 2021 17:04:35 +0900 Subject: [PATCH 13/39] Fix osu!catch fruit showing on plate when hidden mod is enabled Closes https://github.com/ppy/osu/issues/12065. --- osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs | 13 ++++++++++++- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 +++-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 7 ++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 4b008d2734..5c5e41234d 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -3,13 +3,16 @@ using System.Linq; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHidden : ModHidden + public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset { public override string Description => @"Play with fading fruits."; public override double ScoreMultiplier => 1.06; @@ -17,6 +20,14 @@ namespace osu.Game.Rulesets.Catch.Mods private const double fade_out_offset_multiplier = 0.6; private const double fade_out_duration_multiplier = 0.44; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; + var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; + + catchPlayfield.CatcherArea.CatchFruitOnPlate = false; + } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { base.ApplyNormalVisibilityState(hitObject, state); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ed875e7002..42be745c2e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition <= catcherPosition + halfCatchWidth; } - public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result, bool placeOnPlate) { var catchResult = (CatchJudgementResult)result; catchResult.CatcherAnimationState = CurrentState; @@ -237,7 +237,8 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); - placeCaughtObject(palpableObject, positionInStack); + if (placeOnPlate) + placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 44adbd5512..29c95ec61c 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; + /// + /// Whether fruit should appear on the plate. + /// + public bool CatchFruitOnPlate { get; set; } = true; + public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -41,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { - MovableCatcher.OnNewResult(hitObject, result); + MovableCatcher.OnNewResult(hitObject, result, CatchFruitOnPlate); if (!result.Type.IsScorable()) return; From 6808719efabe9cd5997e51f916ecd9f56535b1f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Mar 2021 17:05:28 +0900 Subject: [PATCH 14/39] Update test scene --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 48efd73222..ddb6194899 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.Tests Add(drawableObject); drawableObject.OnLoadComplete += _ => { - catcher.OnNewResult(drawableObject, result); + catcher.OnNewResult(drawableObject, result, true); drawableObject.Expire(); }; } From f6647de7693de11ad68b8316eb17483bd9bb3126 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Mar 2021 19:56:26 +0900 Subject: [PATCH 15/39] Add support for nudging objects in the editor using ctrl+arrow keys Closes #12042. --- .../Components/ComposeBlueprintContainer.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 79f457c050..5ab557804e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Edit.Components.TernaryButtons; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -70,6 +72,50 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.ControlPressed) + { + switch (e.Key) + { + case Key.Left: + moveSelection(new Vector2(-1, 0)); + return true; + + case Key.Right: + moveSelection(new Vector2(1, 0)); + return true; + + case Key.Up: + moveSelection(new Vector2(0, -1)); + return true; + + case Key.Down: + moveSelection(new Vector2(0, 1)); + return true; + } + } + + return false; + } + + /// + /// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints). + /// + /// + private void moveSelection(Vector2 delta) + { + var firstBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(); + + if (firstBlueprint == null) + return; + + // convert to game space coordinates + delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); + + SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta)); + } + private void updatePlacementNewCombo() { if (currentPlacement?.HitObject is IHasComboInformation c) From 5d272bef9778d590b84e1e97edfde3d64a8b68de Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Fri, 26 Mar 2021 16:28:04 +0100 Subject: [PATCH 16/39] Remember ContolPoint positions instead of recalculating them --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index ae92b12fd2..b9578b4ae9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -197,8 +197,13 @@ namespace osu.Game.Rulesets.Osu.Edit Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); + Queue oldControlPoints = new Queue(); + foreach (var point in slider.Path.ControlPoints) + { + oldControlPoints.Enqueue(point.Position.Value); point.Position.Value *= pathRelativeDeltaScale; + } //if sliderhead or sliderend end up outside playfield, revert scaling. Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); @@ -207,8 +212,8 @@ namespace osu.Game.Rulesets.Osu.Edit if (xInBounds && yInBounds) return true; - foreach (var point in slider.Path.ControlPoints) - point.Position.Value *= new Vector2(1 / pathRelativeDeltaScale.X, 1 / pathRelativeDeltaScale.Y); + foreach(var point in slider.Path.ControlPoints) + point.Position.Value = oldControlPoints.Dequeue(); return true; } From 25ea60cb92c6d4e40b7c442d68d2fd17b4a7056d Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Fri, 26 Mar 2021 16:39:13 +0100 Subject: [PATCH 17/39] Remove return values from HandleScale submethods --- .../Edit/OsuSelectionHandler.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index b9578b4ae9..8a657d0f9b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -143,19 +143,18 @@ namespace osu.Game.Rulesets.Osu.Edit adjustScaleFromAnchor(ref scale, reference); var hitObjects = selectedMovableObjects; - bool result; // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) - result = scaleSlider(slider, scale); + scaleSlider(slider, scale); else - result = scaleHitObjects(hitObjects, reference, scale); + scaleHitObjects(hitObjects, reference, scale); moveSelectionInBounds(); - return result; + return true; } private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) @@ -192,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } - private bool scaleSlider(Slider slider, Vector2 scale) + private void scaleSlider(Slider slider, Vector2 scale) { Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); @@ -210,15 +209,13 @@ namespace osu.Game.Rulesets.Osu.Edit (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); if (xInBounds && yInBounds) - return true; + return; foreach(var point in slider.Path.ControlPoints) point.Position.Value = oldControlPoints.Dequeue(); - - return true; } - private bool scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) + private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) { scale = getClampedScale(hitObjects, reference, scale); @@ -241,8 +238,6 @@ namespace osu.Game.Rulesets.Osu.Edit h.Position = newPosition; } - - return true; } private (bool X, bool Y) isQuadInBounds(Quad quad) From 305c2e31cfc7f7e6e30f94d561415619b37c87e0 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Fri, 26 Mar 2021 16:45:05 +0100 Subject: [PATCH 18/39] Clarify todo comment --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8a657d0f9b..24b0d22c1a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -283,7 +283,7 @@ namespace osu.Game.Rulesets.Osu.Edit Quad selectionQuad = getSurroundingQuad(hitObjects); - //todo: this is not always correct for selections involving sliders + //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y); //max Size -> playfield bounds From a50c4be8ab806e4f8875cd286a01d6cf5a3a2a7f Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Fri, 26 Mar 2021 17:41:36 +0100 Subject: [PATCH 19/39] Add missing space --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 24b0d22c1a..3a4a37c8cc 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (xInBounds && yInBounds) return; - foreach(var point in slider.Path.ControlPoints) + foreach (var point in slider.Path.ControlPoints) point.Position.Value = oldControlPoints.Dequeue(); } From 1d99a63f17f8e339097552dc51fbc27bef00b5c7 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 29 Mar 2021 14:02:53 +0200 Subject: [PATCH 20/39] Limit minimum size for single slider scaling --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 3a4a37c8cc..6350bd7190 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -194,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Edit private void scaleSlider(Slider slider, Vector2 scale) { Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + + // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. + scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height); Queue oldControlPoints = new Queue(); From 17b16d4f8914b9989b2f116e334e26fd0a4c39fe Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 29 Mar 2021 14:17:30 +0200 Subject: [PATCH 21/39] Clarify purpose of getClampedScale() --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 6350bd7190..07643bd75e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -274,7 +274,7 @@ namespace osu.Game.Rulesets.Osu.Edit } /// - /// Clamp scale where selection does not exceed playfield bounds or flip. + /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. /// /// The hitobjects to be scaled /// The anchor from which the scale operation is performed From 8a0fcf20ede3a63a074072899b33cb02cd83fdec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 12:32:42 +0900 Subject: [PATCH 22/39] Move offset settings up for more logical ordering --- .../Settings/Sections/Input/TabletSettings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index bd0f7ddc4c..571d79baf8 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -110,12 +110,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input } }, new SettingsSlider - { - TransferValueOnCommit = true, - LabelText = "Aspect Ratio", - Current = aspectRatio - }, - new SettingsSlider { TransferValueOnCommit = true, LabelText = "X Offset", @@ -127,6 +121,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Y Offset", Current = offsetY }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Aspect Ratio", + Current = aspectRatio + }, new SettingsCheckbox { LabelText = "Lock aspect ratio", From 8dfff999f9ced7f2e550df44fcf3c76e871055fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 12:40:50 +0900 Subject: [PATCH 23/39] Add rotation slider --- .../Visual/Settings/TestSceneTabletSettings.cs | 2 ++ .../Settings/Sections/Input/TabletAreaSelection.cs | 9 +++++++++ .../Overlays/Settings/Sections/Input/TabletSettings.cs | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index a7f6c8c0d3..a62980addf 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); + public Bindable Rotation { get; } = new Bindable(); + public IBindable Tablet => tablet; private readonly Bindable tablet = new Bindable(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index ecb8acce54..f61742093c 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); + private readonly BindableNumber rotation = new BindableNumber(); + private readonly IBindable tablet = new Bindable(); private OsuSpriteText tabletName; @@ -124,6 +126,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}"; }, true); + rotation.BindTo(handler.Rotation); + rotation.BindValueChanged(val => + { + usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint) + .OnComplete(_ => checkBounds()); // required as we are using SSDQ. + }); + tablet.BindTo(handler.Tablet); tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails)); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 571d79baf8..9d128f8db3 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber sizeX = new BindableNumber { MinValue = 10 }; private readonly BindableNumber sizeY = new BindableNumber { MinValue = 10 }; + private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + [Resolved] private GameHost host { get; set; } @@ -122,6 +124,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = offsetY }, new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Rotation", + Current = rotation + }, + new SettingsSlider { TransferValueOnCommit = true, LabelText = "Aspect Ratio", @@ -153,6 +161,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); + rotation.BindTo(tabletHandler.Rotation); + areaOffset.BindTo(tabletHandler.AreaOffset); areaOffset.BindValueChanged(val => { From 1dfd08eded521c40576cc746216f3b487e239e9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:01:48 +0900 Subject: [PATCH 24/39] Add tablet rotation configuration --- .../Sections/Input/RotationPresetButtons.cs | 109 ++++++++++++++++++ .../Settings/Sections/Input/TabletSettings.cs | 1 + 2 files changed, 110 insertions(+) create mode 100644 osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs new file mode 100644 index 0000000000..3e8da9f7d0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Handlers.Tablet; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + internal class RotationPresetButtons : FillFlowContainer + { + private readonly ITabletHandler tabletHandler; + + private Bindable rotation; + + private const int height = 50; + + public RotationPresetButtons(ITabletHandler tabletHandler) + { + this.tabletHandler = tabletHandler; + + RelativeSizeAxes = Axes.X; + Height = height; + + for (int i = 0; i < 360; i += 90) + { + var presetRotation = i; + + Add(new RotationButton(i) + { + RelativeSizeAxes = Axes.X, + Height = height, + Width = 0.25f, + Text = $"{presetRotation}ยบ", + Action = () => tabletHandler.Rotation.Value = presetRotation, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rotation = tabletHandler.Rotation.GetBoundCopy(); + rotation.BindValueChanged(val => + { + foreach (var b in Children.OfType()) + b.IsSelected = b.Preset == val.NewValue; + }, true); + } + + public class RotationButton : TriangleButton + { + [Resolved] + private OsuColour colours { get; set; } + + public readonly int Preset; + + public RotationButton(int preset) + { + Preset = preset; + } + + private bool isSelected; + + public bool IsSelected + { + get => isSelected; + set + { + if (value == isSelected) + return; + + isSelected = value; + + if (IsLoaded) + updateColour(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColour(); + } + + private void updateColour() + { + if (isSelected) + { + BackgroundColour = colours.BlueDark; + Triangles.ColourDark = colours.BlueDarker; + Triangles.ColourLight = colours.Blue; + } + else + { + BackgroundColour = colours.Gray4; + Triangles.ColourDark = colours.Gray5; + Triangles.ColourLight = colours.Gray6; + } + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9d128f8db3..d770c18878 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -129,6 +129,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Rotation", Current = rotation }, + new RotationPresetButtons(tabletHandler), new SettingsSlider { TransferValueOnCommit = true, From b82247aabec2ee10c12105701d1c0e073a775e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:13:16 +0900 Subject: [PATCH 25/39] Add inline comments and use Vector2.Zero --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 07643bd75e..82301bd37f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -39,9 +39,11 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; + // this will potentially move the selection out of bounds... foreach (var h in hitObjects) h.Position += moveEvent.InstantDelta; + // but this will be corrected. moveSelectionInBounds(); return true; } @@ -153,7 +155,6 @@ namespace osu.Game.Rulesets.Osu.Edit scaleHitObjects(hitObjects, reference, scale); moveSelectionInBounds(); - return true; } @@ -257,7 +258,8 @@ namespace osu.Game.Rulesets.Osu.Edit var hitObjects = selectedMovableObjects; Quad quad = getSurroundingQuad(hitObjects); - Vector2 delta = new Vector2(0); + + Vector2 delta = Vector2.Zero; if (quad.TopLeft.X < 0) delta.X -= quad.TopLeft.X; From 88035f73e00229ca5587adb916dc36716da29fb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:23:46 +0900 Subject: [PATCH 26/39] Fix incorrect wait logic in IPC location test Not really willing to put more effort into fixing this one. Should do the job. --- .../NonVisual/IPCLocationTest.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs index 4791da93c6..4c5f5a7a1a 100644 --- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs @@ -26,26 +26,26 @@ namespace osu.Game.Tournament.Tests.NonVisual string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(CheckIPCLocation)); // Set up a fake IPC client for the IPC Storage to switch to. - string testCeDir = Path.Combine(basePath, "stable-ce"); - Directory.CreateDirectory(testCeDir); + string testStableInstallDirectory = Path.Combine(basePath, "stable-ce"); + Directory.CreateDirectory(testStableInstallDirectory); - string ipcFile = Path.Combine(testCeDir, "ipc.txt"); + string ipcFile = Path.Combine(testStableInstallDirectory, "ipc.txt"); File.WriteAllText(ipcFile, string.Empty); try { var osu = loadOsu(host); TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get(); - FileBasedIPC ipc = (FileBasedIPC)osu.Dependencies.Get(); + FileBasedIPC ipc = null; - waitForOrAssert(() => ipc != null, @"ipc could not be populated in a reasonable amount of time"); + waitForOrAssert(() => (ipc = osu.Dependencies.Get() as FileBasedIPC) != null, @"ipc could not be populated in a reasonable amount of time"); - Assert.True(ipc.SetIPCLocation(testCeDir)); + Assert.True(ipc.SetIPCLocation(testStableInstallDirectory)); Assert.True(storage.AllTournaments.Exists("stable.json")); } finally { - host.Storage.DeleteDirectory(testCeDir); + host.Storage.DeleteDirectory(testStableInstallDirectory); host.Storage.DeleteDirectory("tournaments"); host.Exit(); } From 89bea2868a9ca68a8c09a9c93db6f504e9022a96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:33:55 +0900 Subject: [PATCH 27/39] Move bool one level down --- osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 9 +++++++-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 7 +------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 5c5e41234d..bba42dea97 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Mods var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; - catchPlayfield.CatcherArea.CatchFruitOnPlate = false; + catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false; } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 42be745c2e..5d57e84b75 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public bool HyperDashing => hyperDashModifier != 1; + /// + /// Whether fruit should appear on the plate. + /// + public bool CatchFruitOnPlate { get; set; } = true; + /// /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// @@ -223,7 +228,7 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition <= catcherPosition + halfCatchWidth; } - public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result, bool placeOnPlate) + public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) { var catchResult = (CatchJudgementResult)result; catchResult.CatcherAnimationState = CurrentState; @@ -237,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); - if (placeOnPlate) + if (CatchFruitOnPlate) placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 29c95ec61c..44adbd5512 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; - /// - /// Whether fruit should appear on the plate. - /// - public bool CatchFruitOnPlate { get; set; } = true; - public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { - MovableCatcher.OnNewResult(hitObject, result, CatchFruitOnPlate); + MovableCatcher.OnNewResult(hitObject, result); if (!result.Type.IsScorable()) return; From f12353a99e5b38c57d1578460b542b27d07a1cec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:57:06 +0900 Subject: [PATCH 28/39] Update forgotten test scene usage --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index ddb6194899..48efd73222 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.Tests Add(drawableObject); drawableObject.OnLoadComplete += _ => { - catcher.OnNewResult(drawableObject, result, true); + catcher.OnNewResult(drawableObject, result); drawableObject.Expire(); }; } From 1d968009c2c329a1abd107409c9e37c302ae36c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:07:04 +0900 Subject: [PATCH 29/39] Add osu!mania key filtering using "keys=4" at song select --- .../Beatmaps/ManiaBeatmapConverter.cs | 8 ++++- .../ManiaFilterCriteria.cs | 33 +++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 7a0e3b2b76..756207a201 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps if (IsForCurrentRuleset) { - TargetColumns = (int)Math.Max(1, roundedCircleSize); + TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo); if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } + internal static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + { + var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + return (int)Math.Max(1, roundedCircleSize); + } + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs new file mode 100644 index 0000000000..c9a1ae84d4 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaFilterCriteria : IRulesetFilterCriteria + { + private FilterCriteria.OptionalRange keys; + + public bool Matches(BeatmapInfo beatmap) + { + return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID) && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)); + } + + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + { + switch (key) + { + case "key": + case "keys": + return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value); + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index d624e094ad..88b63606b9 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; @@ -382,6 +383,11 @@ namespace osu.Game.Rulesets.Mania } } }; + + public override IRulesetFilterCriteria CreateRulesetFilterCriteria() + { + return new ManiaFilterCriteria(); + } } public enum PlayfieldType From e769ef45be36d1bf700969bf76bcab5c0dac2f99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:55:39 +0900 Subject: [PATCH 30/39] Fix misplaced parenthesis --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index c9a1ae84d4..d9a278ef29 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania public bool Matches(BeatmapInfo beatmap) { - return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID) && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)); + return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) From 56428a027e53e9e89dcb321a5f58eb32c899d52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:56:19 +0900 Subject: [PATCH 31/39] Change static method to public --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 756207a201..26393c8edb 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } - internal static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) { var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); From 633e6130bf434c628d7be98eeba0f0fff9d30cf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 19:45:22 +0900 Subject: [PATCH 32/39] 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 3682a44b9f..5b65670869 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 6d571218fc..6a7f7e7026 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 ceb46eae87..4aa3ad1c61 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From fb0079fb9f641c7a7c769a3144684cb90c0320a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 22:42:31 +0900 Subject: [PATCH 33/39] Fix accuracy displaying incorrectly in online contexts Closes #12221. --- osu.Game/Users/UserStatistics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 04a358436e..5ddcd86d28 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -45,7 +45,7 @@ namespace osu.Game.Users public double Accuracy; [JsonIgnore] - public string DisplayAccuracy => Accuracy.FormatAccuracy(); + public string DisplayAccuracy => (Accuracy / 100).FormatAccuracy(); [JsonProperty(@"play_count")] public int PlayCount; From ded91b32a4facc2e55d909428999401ffec0ac80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:11:43 +0900 Subject: [PATCH 34/39] Add failing test --- .../TestSceneHoldNoteInput.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 7ae69bf7d7..2357778948 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -288,6 +288,42 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => j.Type.IsHit())); } + [Test] + public void TestHitTailBeforeLastTick() + { + const int tick_rate = 8; + const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate; + const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1); + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_last_tick - 5) + }, beatmap); + + assertHeadJudgement(HitResult.Perfect); + AddAssert("one tick missed", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Count(j => j.Type == HitResult.LargeTickMiss) == 1); + assertTailJudgement(HitResult.Ok); + } + private void assertHeadJudgement(HitResult result) => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); From f78d628878a0244c1de1b0efa26d9e206d593771 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:21:07 +0900 Subject: [PATCH 35/39] Improve assertions --- .../TestSceneHoldNoteInput.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 2357778948..42ea12214f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -320,21 +320,24 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Perfect); - AddAssert("one tick missed", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Count(j => j.Type == HitResult.LargeTickMiss) == 1); + assertLastTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Ok); } private void assertHeadJudgement(HitResult result) - => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); + => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result); private void assertTailJudgement(HitResult result) - => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); + => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result); private void assertNoteJudgement(HitResult result) - => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result); + => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result); private void assertTickJudgement(HitResult result) - => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick + => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result)); + + private void assertLastTickJudgement(HitResult result) + => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result); private ScoreAccessibleReplayPlayer currentPlayer; From 43e48406caa2253ccdc580c53ad22d0b39ad7ebf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:21:14 +0900 Subject: [PATCH 36/39] Miss all ticks when hold note is hit --- .../Objects/Drawables/DrawableHoldNote.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 4f062753a6..828ee7b03e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,6 +233,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { + foreach (var tick in tickContainer) + { + if (!tick.Judged) + tick.MissForcefully(); + } + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } From e0c61f4dc556189de3e841242568df93269aafdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 13:57:57 +0900 Subject: [PATCH 37/39] Fix retry count not updating correctly Regressed with changes to player reference retention logic. Could add a test but the logic is so local now it seems quite redundant. --- osu.Game/Screens/Play/PlayerLoader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 7d906cdc5b..2bbc4a0469 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -309,10 +309,8 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - var restartCount = player?.RestartCount + 1 ?? 0; - player = createPlayer(); - player.RestartCount = restartCount; + player.RestartCount = ++restartCount; player.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false); @@ -428,6 +426,8 @@ namespace osu.Game.Screens.Play private Bindable muteWarningShownOnce; + private int restartCount; + private void showMuteWarningIfNeeded() { if (!muteWarningShownOnce.Value) From 0c53b4eb938a2770c5b2420dcf646cce0bd16c23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 14:09:38 +0900 Subject: [PATCH 38/39] Fix wrong counting and add test --- .../Navigation/TestSceneScreenNavigation.cs | 23 +++++++++++++++++++ osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f2bb518b2e..3e25e22b5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -43,6 +43,29 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + [Test] + public void TestRetryCountIncrements() + { + Player player = null; + + PushAndConfirm(() => new TestSongSelect()); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 0", () => player.RestartCount == 0); + + AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); + + AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 1", () => player.RestartCount == 1); + } + [Test] public void TestRetryFromResults() { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 2bbc4a0469..679b3c7313 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Play return; player = createPlayer(); - player.RestartCount = ++restartCount; + player.RestartCount = restartCount++; player.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false); From 30cae46cbdf44021e850f63122d90d803052ca9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 14:57:28 +0900 Subject: [PATCH 39/39] Group large drag drop imports into a single operation --- osu.Desktop/OsuGameDesktop.cs | 34 +++++++++++++++++++++++++++++++--- osu.Game/OsuGameBase.cs | 3 +++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b2487568ce..0c21c75290 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.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.IO; using System.Linq; using System.Reflection; @@ -18,6 +19,7 @@ using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Framework.Threading; using osu.Game.IO; namespace osu.Desktop @@ -144,13 +146,39 @@ namespace osu.Desktop desktopWindow.DragDrop += f => fileDrop(new[] { f }); } + private readonly List importableFiles = new List(); + private ScheduledDelegate importSchedule; + private void fileDrop(string[] filePaths) { - var firstExtension = Path.GetExtension(filePaths.First()); + lock (importableFiles) + { + var firstExtension = Path.GetExtension(filePaths.First()); - if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; - Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); + importableFiles.AddRange(filePaths); + + Logger.Log($"Adding {filePaths.Length} files for import"); + + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + importSchedule?.Cancel(); + importSchedule = Scheduler.AddDelayed(handlePendingImports, 100); + } + } + + private void handlePendingImports() + { + lock (importableFiles) + { + Logger.Log($"Handling batch import of {importableFiles.Count} files"); + + var paths = importableFiles.ToArray(); + importableFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e1c7b67a8c..7eef0f7158 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -429,6 +429,9 @@ namespace osu.Game public async Task Import(params string[] paths) { + if (paths.Length == 0) + return; + var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); foreach (var importer in fileImporters)