From cb6339a20b50d1c8f250f0ca1588bb03c5341963 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 18 Aug 2022 01:29:03 +0200 Subject: [PATCH 01/20] added slider splitting option --- .../Components/PathControlPointVisualiser.cs | 48 +++++++++++-- .../Sliders/SliderSelectionBlueprint.cs | 70 ++++++++++++++++++- 2 files changed, 111 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 0506e8ab8a..3fb7ec93e6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private InputManager inputManager; public Action> RemoveControlPointsRequested; + public Action> SplitControlPointsRequested; [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -104,6 +105,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } + // The slider can only be split on control points which connect two different slider segments. + private bool splittable(PathControlPointPiece p) => p.ControlPoint.Type.HasValue && p != Pieces[0] && p != Pieces[^1]; + + private bool splitSelected() + { + List toSplit = Pieces.Where(p => p.IsSelected.Value && splittable(p)).Select(p => p.ControlPoint).ToList(); + + // Ensure that there are any points to be split + if (toSplit.Count == 0) + return false; + + changeHandler?.BeginChange(); + SplitControlPointsRequested?.Invoke(toSplit); + changeHandler?.EndChange(); + + // Since pieces are re-used, they will not point to the deleted control points while remaining selected + foreach (var piece in Pieces) + piece.IsSelected.Value = false; + + return true; + } + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -322,6 +345,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (count == 0) return null; + var splittablePieces = selectedPieces.Where(splittable).ToList(); + int splittableCount = splittablePieces.Count; + List items = new List(); if (!selectedPieces.Contains(Pieces[0])) @@ -333,14 +359,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components items.Add(createMenuItemForPathType(PathType.Bezier)); items.Add(createMenuItemForPathType(PathType.Catmull)); - return new MenuItem[] + var menuItems = new MenuItem[splittableCount > 0 ? 3 : 2]; + int i = 0; + + menuItems[i++] = new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, + () => DeleteSelected()); + + if (splittableCount > 0) { - new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()), - new OsuMenuItem("Curve type") - { - Items = items - } + menuItems[i++] = new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, () => splitSelected()); + } + + menuItems[i] = new OsuMenuItem("Curve type") + { + Items = items }; + + return menuItems; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 794551dab7..70993ef7ac 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true) { - RemoveControlPointsRequested = removeControlPoints + RemoveControlPointsRequested = removeControlPoints, + SplitControlPointsRequested = splitControlPoints }); base.OnSelected(); @@ -249,6 +251,72 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Position += first; } + private void splitControlPoints(List toSplit) + { + // Ensure that there are any points to be split + if (toSplit.Count == 0) + return; + + foreach (var c in toSplit) + { + if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) + continue; + + // Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider. + var splitControlPoints = controlPoints.TakeWhile(current => current != c).ToList(); + + if (splitControlPoints.Count == 0) + continue; + + foreach (var current in splitControlPoints) + { + controlPoints.Remove(current); + } + + splitControlPoints.Add(c); + + // Turn the control points which were split off into a new slider. + var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); + samplePoint.Time = HitObject.StartTime; + var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone(); + difficultyPoint.Time = HitObject.StartTime; + + var newSlider = new Slider + { + StartTime = HitObject.StartTime, + Position = HitObject.Position + splitControlPoints[0].Position, + NewCombo = HitObject.NewCombo, + SampleControlPoint = samplePoint, + DifficultyControlPoint = difficultyPoint, + Samples = HitObject.Samples.Select(s => s.With()).ToList(), + RepeatCount = HitObject.RepeatCount, + NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), + Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) + }; + + editorBeatmap.Add(newSlider); + + HitObject.NewCombo = false; + HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; + HitObject.StartTime += newSlider.SpanDuration; + + // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. + if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) + { + HitObject.Path.ExpectedDistance.Value = null; + } + } + + editorBeatmap.SelectedHitObjects.Clear(); + + // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position + // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) + Vector2 first = controlPoints[0].Position; + foreach (var c in controlPoints) + c.Position -= first; + HitObject.Position += first; + } + private void convertToStream() { if (editorBeatmap == null || beatDivisor == null) From 89eb0a4079c4b5564e47623008ff81067a2e4a08 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 01:10:54 +0200 Subject: [PATCH 02/20] Added TestScene for slider splitting --- .../Editor/TestSceneSliderSplitting.cs | 127 ++++++++++++++++++ .../Sliders/SliderSelectionBlueprint.cs | 8 +- 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs new file mode 100644 index 0000000000..4693d56789 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -0,0 +1,127 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +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.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneSliderSplitting : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private ComposeBlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); + + private ContextMenuContainer contextMenuContainer + => Editor.ChildrenOfType().First(); + + private Slider? slider; + private PathControlPointVisualiser? visualiser; + + [Test] + public void TestBasicSplit() + { + AddStep("add slider", () => + { + slider = new Slider + { + Position = new Vector2(0, 50), + 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)) + }) + }; + + EditorBeatmap.Add(slider); + }); + + AddStep("select added slider", () => + { + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); + }); + + moveMouseToControlPoint(2); + AddStep("select control point", () => + { + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Split control point"); + } + + [Test] + public void TestStartTimeOffsetPlusDeselect() + { + HitCircle? circle = null; + + AddStep("add circle", () => + { + circle = new HitCircle(); + + EditorBeatmap.Add(circle); + }); + + AddStep("select added circle", () => + { + EditorBeatmap.SelectedHitObjects.Add(circle); + }); + + AddStep("add another circle", () => + { + var circle2 = new HitCircle(); + + EditorBeatmap.Add(circle2); + }); + + AddStep("change time of selected circle and deselect", () => + { + if (circle is null) return; + + circle.StartTime += 1; + EditorBeatmap.SelectedHitObjects.Clear(); + }); + } + + private void moveMouseToControlPoint(int index) + { + AddStep($"move mouse to control point {index}", () => + { + if (slider is null || visualiser is null) return; + + Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position; + InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); + }); + } + + private void addContextMenuItemStep(string contextMenuText) + { + AddStep($"click context menu item \"{contextMenuText}\"", () => + { + if (visualiser is null) return; + + MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + + item?.Action?.Value(); + }); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 70993ef7ac..e28dbb485d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -294,11 +294,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) }; + HitObject.StartTime += 1; + editorBeatmap.Add(newSlider); HitObject.NewCombo = false; HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; - HitObject.StartTime += newSlider.SpanDuration; + HitObject.StartTime += newSlider.SpanDuration - 1; // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) @@ -307,14 +309,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - editorBeatmap.SelectedHitObjects.Clear(); - // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) Vector2 first = controlPoints[0].Position; foreach (var c in controlPoints) c.Position -= first; HitObject.Position += first; + + editorBeatmap.SelectedHitObjects.Clear(); } private void convertToStream() From d1519343f6040c2f0752956ad406f3c42152f731 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 18:29:01 +0200 Subject: [PATCH 03/20] Improved visual tests for slider splitting --- .../Editor/TestSceneSliderSplitting.cs | 108 ++++++++++++++---- .../Sliders/SliderSelectionBlueprint.cs | 1 + 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 4693d56789..198d521a85 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -3,9 +3,9 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -27,15 +27,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private ComposeBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); - private ContextMenuContainer contextMenuContainer - => Editor.ChildrenOfType().First(); - private Slider? slider; private PathControlPointVisualiser? visualiser; [Test] public void TestBasicSplit() { + double endTime = 0; + AddStep("add slider", () => { slider = new Slider @@ -52,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }; EditorBeatmap.Add(slider); + + endTime = slider.EndTime; }); AddStep("select added slider", () => @@ -66,39 +67,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; }); addContextMenuItemStep("Split control point"); + + AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, slider.StartTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime, + (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + )); + + AddStep("undo", () => Editor.Undo()); + AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + )); } [Test] - public void TestStartTimeOffsetPlusDeselect() + public void TestDoubleSplit() { - HitCircle? circle = null; + double endTime = 0; - AddStep("add circle", () => + AddStep("add slider", () => { - circle = new HitCircle(); + slider = new Slider + { + Position = new Vector2(0, 50), + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(new Vector2(150, 150)), + new PathControlPoint(new Vector2(300, 0), PathType.Bezier), + new PathControlPoint(new Vector2(400, 0)), + new PathControlPoint(new Vector2(400, 150), PathType.Catmull), + new PathControlPoint(new Vector2(300, 200)), + new PathControlPoint(new Vector2(400, 250)) + }) + }; - EditorBeatmap.Add(circle); + EditorBeatmap.Add(slider); + + endTime = slider.EndTime; }); - AddStep("select added circle", () => + AddStep("select added slider", () => { - EditorBeatmap.SelectedHitObjects.Add(circle); + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); }); - AddStep("add another circle", () => + moveMouseToControlPoint(2); + AddStep("select first control point", () => { - var circle2 = new HitCircle(); - - EditorBeatmap.Add(circle2); + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; }); - - AddStep("change time of selected circle and deselect", () => + moveMouseToControlPoint(4); + AddStep("select second control point", () => { - if (circle is null) return; - - circle.StartTime += 1; - EditorBeatmap.SelectedHitObjects.Clear(); + if (visualiser is not null) visualiser.Pieces[4].IsSelected.Value = true; }); + addContextMenuItemStep("Split 2 control points"); + + AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime, + (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(150, 200), null), + (new Vector2(300, 50), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime(), slider.StartTime, + (new Vector2(300, 50), PathType.Bezier), + (new Vector2(400, 50), null), + (new Vector2(400, 200), null) + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime(), endTime, + (new Vector2(400, 200), PathType.Catmull), + (new Vector2(300, 250), null), + (new Vector2(400, 300), null) + )); + } + + private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) + { + if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false; + + int i = 0; + + foreach ((Vector2 pos, PathType? pathType) in expectedControlPoints) + { + var controlPoint = s.Path.ControlPoints[i++]; + + if (!Precision.AlmostEquals(controlPoint.Position + s.Position, pos) || controlPoint.Type != pathType) + return false; + } + + return true; } private void moveMouseToControlPoint(int index) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index e28dbb485d..d903ada6e2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -294,6 +294,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Path = new SliderPath(splitControlPoints.Select(o => new PathControlPoint(o.Position - splitControlPoints[0].Position, o == splitControlPoints[^1] ? null : o.Type)).ToArray()) }; + // Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid. HitObject.StartTime += 1; editorBeatmap.Add(newSlider); From 91e6f4c4eefeac62597e5ce18da8f6a8a0dceab7 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 19 Aug 2022 19:31:47 +0200 Subject: [PATCH 04/20] fix TestPerfectCurveChangeToBezier --- .../Editor/TestScenePathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index f196353f87..6d93c3fcf9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep($"click context menu item \"{contextMenuText}\"", () => { - MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); + MenuItem item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText); item?.Action?.Value(); }); From 885ea4270b5bbad78a48690abdaa5f95e136b5c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Aug 2022 14:03:54 +0900 Subject: [PATCH 05/20] Reorder context menu items and tidy up surrounding code --- .../Components/PathControlPointVisualiser.cs | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 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 3fb7ec93e6..48e1d6405d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -105,12 +105,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } - // The slider can only be split on control points which connect two different slider segments. - private bool splittable(PathControlPointPiece p) => p.ControlPoint.Type.HasValue && p != Pieces[0] && p != Pieces[^1]; - private bool splitSelected() { - List toSplit = Pieces.Where(p => p.IsSelected.Value && splittable(p)).Select(p => p.ControlPoint).ToList(); + List toSplit = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); // Ensure that there are any points to be split if (toSplit.Count == 0) @@ -127,6 +124,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return true; } + private bool isSplittable(PathControlPointPiece p) => + // A slider can only be split on control points which connect two different slider segments. + p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) @@ -345,38 +346,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (count == 0) return null; - var splittablePieces = selectedPieces.Where(splittable).ToList(); + var splittablePieces = selectedPieces.Where(isSplittable).ToList(); int splittableCount = splittablePieces.Count; - List items = new List(); + List curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) - items.Add(createMenuItemForPathType(null)); + curveTypeItems.Add(createMenuItemForPathType(null)); // todo: hide/disable items which aren't valid for selected points - items.Add(createMenuItemForPathType(PathType.Linear)); - items.Add(createMenuItemForPathType(PathType.PerfectCurve)); - items.Add(createMenuItemForPathType(PathType.Bezier)); - items.Add(createMenuItemForPathType(PathType.Catmull)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); + curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier)); + curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull)); - var menuItems = new MenuItem[splittableCount > 0 ? 3 : 2]; - int i = 0; - - menuItems[i++] = new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, - () => DeleteSelected()); + var menuItems = new List + { + new OsuMenuItem("Curve type") + { + Items = curveTypeItems + } + }; if (splittableCount > 0) { - menuItems[i++] = new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", - MenuItemType.Destructive, () => splitSelected()); + menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, + () => splitSelected())); } - menuItems[i] = new OsuMenuItem("Curve type") - { - Items = items - }; + menuItems.Add( + new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", + MenuItemType.Destructive, + () => DeleteSelected()) + ); - return menuItems; + return menuItems.ToArray(); } } From 1f9cdff0139a84830cc099537b2cad7e9552c3f6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 23 Aug 2022 22:19:40 +0200 Subject: [PATCH 06/20] remove these lines --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d903ada6e2..de40d8e03b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -277,9 +277,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders // Turn the control points which were split off into a new slider. var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); - samplePoint.Time = HitObject.StartTime; var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone(); - difficultyPoint.Time = HitObject.StartTime; var newSlider = new Slider { From 631ea9a3edaf0ba8c27b685be971521a07931fbf Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 23 Aug 2022 23:28:50 +0200 Subject: [PATCH 07/20] added a gap between objects and made it theoretically possible to retain sample control point --- .../Editor/TestSceneSliderSplitting.cs | 72 +++++++++++++++++-- .../Sliders/SliderSelectionBlueprint.cs | 8 ++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index 198d521a85..015952c59a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? slider; private PathControlPointVisualiser? visualiser; + private const double split_gap = 100; + [Test] public void TestBasicSplit() { @@ -69,11 +72,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Split control point"); AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && - sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, slider.StartTime, + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, (new Vector2(0, 50), PathType.PerfectCurve), (new Vector2(150, 200), null), (new Vector2(300, 50), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap, (new Vector2(300, 50), PathType.PerfectCurve), (new Vector2(400, 50), null), (new Vector2(400, 200), null) @@ -135,21 +138,80 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Split 2 control points"); AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && - sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime, + sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, (new Vector2(0, 50), PathType.PerfectCurve), (new Vector2(150, 200), null), (new Vector2(300, 50), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime(), slider.StartTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap, (new Vector2(300, 50), PathType.Bezier), (new Vector2(400, 50), null), (new Vector2(400, 200), null) - ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime(), endTime, + ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2, (new Vector2(400, 200), PathType.Catmull), (new Vector2(300, 250), null), (new Vector2(400, 300), null) )); } + [Test] + public void TestSplitRetainsHitsounds() + { + HitSampleInfo? sample = null; + + AddStep("add slider", () => + { + slider = new Slider + { + Position = new Vector2(0, 50), + LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point + 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)) + }) + }; + + EditorBeatmap.Add(slider); + }); + + AddStep("add hitsounds", () => + { + if (slider is null) return; + + slider.SampleControlPoint.SampleBank = "soft"; + slider.SampleControlPoint.SampleVolume = 70; + sample = new HitSampleInfo("hitwhistle"); + slider.Samples.Add(sample); + }); + + AddStep("select added slider", () => + { + EditorBeatmap.SelectedHitObjects.Add(slider); + visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType().First(); + }); + + moveMouseToControlPoint(2); + AddStep("select control point", () => + { + if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Split control point"); + AddAssert("sliders have hitsounds", hasHitsounds); + + AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); + AddStep("remove first slider", () => EditorBeatmap.RemoveAt(0)); + AddStep("undo", () => Editor.Undo()); + AddAssert("sliders have hitsounds", hasHitsounds); + + bool hasHitsounds() => sample is not null && + EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && + o.SampleControlPoint.SampleVolume == 70 && + o.Samples.Contains(sample)); + } + private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) { if (!Precision.AlmostEquals(s.StartTime, startTime, 1) || !Precision.AlmostEquals(s.EndTime, endTime, 1)) return false; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index de40d8e03b..14ed305b25 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -253,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void splitControlPoints(List toSplit) { + // Arbitrary gap in milliseconds to put between split slider pieces + const double split_gap = 100; + // Ensure that there are any points to be split if (toSplit.Count == 0) return; @@ -286,6 +289,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders NewCombo = HitObject.NewCombo, SampleControlPoint = samplePoint, DifficultyControlPoint = difficultyPoint, + LegacyLastTickOffset = HitObject.LegacyLastTickOffset, Samples = HitObject.Samples.Select(s => s.With()).ToList(), RepeatCount = HitObject.RepeatCount, NodeSamples = HitObject.NodeSamples.Select(n => (IList)n.Select(s => s.With()).ToList()).ToList(), @@ -293,13 +297,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; // Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid. - HitObject.StartTime += 1; + HitObject.StartTime += split_gap; editorBeatmap.Add(newSlider); HitObject.NewCombo = false; HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; - HitObject.StartTime += newSlider.SpanDuration - 1; + HitObject.StartTime += newSlider.SpanDuration; // In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider. if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON) From 5ec95c92694f47ca376ff6a3e54d4b66fee5410e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 24 Aug 2022 19:46:35 +0900 Subject: [PATCH 08/20] Update ScoreProcessor to make use of MaximumStatistics --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9e7a5e3657..866f285e4f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -535,6 +535,9 @@ namespace osu.Game.Rulesets.Scoring { extractScoringValues(scoreInfo.Statistics, out current, out maximum); current.MaxCombo = scoreInfo.MaxCombo; + + if (scoreInfo.MaximumStatistics.Count > 0) + extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum); } /// From c9ff39f8c33d014462db360d6c19d45653ebf32c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 23 Aug 2022 21:00:30 +0900 Subject: [PATCH 09/20] Add HitResult.LegacyComboIncrease --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 69 +++++++++++++++++++ osu.Game/Rulesets/Scoring/HitResult.cs | 38 ++++++++-- .../Rulesets/Scoring/JudgementProcessor.cs | 5 ++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 3 +- 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index ff282fff62..63d6e0483b 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -314,6 +315,46 @@ namespace osu.Game.Tests.Rulesets.Scoring }), Is.EqualTo(expectedScore).Within(0.5d)); } +#pragma warning disable CS0618 + [Test] + public void TestLegacyComboIncrease() + { + Assert.That(HitResult.LegacyComboIncrease.IncreasesCombo(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.BreaksCombo(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.AffectsCombo(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.AffectsAccuracy(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsBasic(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsTick(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsBonus(), Is.False); + Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True); + Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True); + Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease)); + + // Cannot be used to apply results. + Assert.Throws(() => scoreProcessor.ApplyBeatmap(new Beatmap + { + HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) } + })); + + ScoreInfo testScore = new ScoreInfo + { + MaxCombo = 1, + Statistics = new Dictionary + { + { HitResult.Great, 1 } + }, + MaximumStatistics = new Dictionary + { + { HitResult.Great, 1 }, + { HitResult.LegacyComboIncrease, 1 } + } + }; + + double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore); + Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%). + } +#pragma warning restore CS0618 + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException(); @@ -352,5 +393,33 @@ namespace osu.Game.Tests.Rulesets.Scoring this.maxResult = maxResult; } } + + private class TestScoreProcessor : ScoreProcessor + { + protected override double DefaultAccuracyPortion => 0.5; + protected override double DefaultComboPortion => 0.5; + + public TestScoreProcessor() + : base(new TestRuleset()) + { + } + + // ReSharper disable once MemberHidesStaticFromOuterClass + private class TestRuleset : Ruleset + { + protected override IEnumerable GetValidHitResults() => new[] { HitResult.Great }; + + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description => string.Empty; + public override string ShortName => string.Empty; + } + } } } diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index bfa256fc20..e6aba4a70e 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -119,8 +119,20 @@ namespace osu.Game.Rulesets.Scoring [EnumMember(Value = "ignore_hit")] [Order(12)] IgnoreHit, + + /// + /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not contribute to score. + /// + /// + /// DO NOT USE. + /// + [EnumMember(Value = "legacy_combo_increase")] + [Order(99)] + [Obsolete("Do not use.")] + LegacyComboIncrease = 99 } +#pragma warning disable CS0618 public static class HitResultExtensions { /// @@ -150,6 +162,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.Perfect: case HitResult.LargeTickHit: case HitResult.LargeTickMiss: + case HitResult.LegacyComboIncrease: return true; default: @@ -161,13 +174,23 @@ namespace osu.Game.Rulesets.Scoring /// Whether a affects the accuracy portion of the score. /// public static bool AffectsAccuracy(this HitResult result) - => IsScorable(result) && !IsBonus(result); + { + if (result == HitResult.LegacyComboIncrease) + return false; + + return IsScorable(result) && !IsBonus(result); + } /// /// Whether a is a non-tick and non-bonus result. /// public static bool IsBasic(this HitResult result) - => IsScorable(result) && !IsTick(result) && !IsBonus(result); + { + if (result == HitResult.LegacyComboIncrease) + return false; + + return IsScorable(result) && !IsTick(result) && !IsBonus(result); + } /// /// Whether a should be counted as a tick. @@ -225,12 +248,18 @@ namespace osu.Game.Rulesets.Scoring /// /// Whether a is scorable. /// - public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; + public static bool IsScorable(this HitResult result) + { + if (result == HitResult.LegacyComboIncrease) + return true; + + return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + } /// /// An array of all scorable s. /// - public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray(); + public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Except(new[] { HitResult.LegacyComboIncrease }).ToArray(); /// /// Whether a is valid within a given range. @@ -251,4 +280,5 @@ namespace osu.Game.Rulesets.Scoring return result > minResult && result < maxResult; } } +#pragma warning restore CS0618 } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 12fe0056bb..bc8f2c22f3 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -61,6 +61,11 @@ namespace osu.Game.Rulesets.Scoring /// The to apply. public void ApplyResult(JudgementResult result) { +#pragma warning disable CS0618 + if (result.Type == HitResult.LegacyComboIncrease) + throw new ArgumentException(@$"A {nameof(HitResult.LegacyComboIncrease)} hit result cannot be applied."); +#pragma warning restore CS0618 + JudgedHits++; lastAppliedResult = result; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 866f285e4f..905fda6d8b 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -593,7 +593,8 @@ namespace osu.Game.Rulesets.Scoring if (result.IsBonus()) current.BonusScore += count * Judgement.ToNumericResult(result); - else + + if (result.AffectsAccuracy()) { // The maximum result of this judgement if it wasn't a miss. // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT). From 73f41439aeffd5036fe54038035b67d8d8819aac Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 13:34:58 +0900 Subject: [PATCH 10/20] Remove redundant qualifiers --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 63d6e0483b..44ebdad2e4 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -357,13 +357,13 @@ namespace osu.Game.Tests.Rulesets.Scoring private class TestRuleset : Ruleset { - public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException(); + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException(); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); - public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException(); + public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException(); public override string Description => string.Empty; public override string ShortName => string.Empty; From 091c51e6646935e0b5616b9b00c11d58462aa3ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 15:00:32 +0900 Subject: [PATCH 11/20] Fix `SliderPath.Version` bindings not being correctly cleaned up on path changes --- .../Sliders/Components/PathControlPointVisualiser.cs | 6 ++++-- 1 file changed, 4 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 0506e8ab8a..0febfba5f2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -142,8 +142,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case NotifyCollectionChangedAction.Remove: foreach (var point in e.OldItems.Cast()) { - Pieces.RemoveAll(p => p.ControlPoint == point); - Connections.RemoveAll(c => c.ControlPoint == point); + foreach (var piece in Pieces.Where(p => p.ControlPoint == point)) + piece.RemoveAndDisposeImmediately(); + foreach (var connection in Connections.Where(c => c.ControlPoint == point)) + connection.RemoveAndDisposeImmediately(); } // If removing before the end of the path, From 3ca4bdc0878d220c3ba4978c0e5fc2421c037eb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Aug 2022 15:13:38 +0900 Subject: [PATCH 12/20] Add `ToArray()` calls to removal iteration for safety --- .../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 0febfba5f2..c1908ef44c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -142,9 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components case NotifyCollectionChangedAction.Remove: foreach (var point in e.OldItems.Cast()) { - foreach (var piece in Pieces.Where(p => p.ControlPoint == point)) + foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) piece.RemoveAndDisposeImmediately(); - foreach (var connection in Connections.Where(c => c.ControlPoint == point)) + foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray()) connection.RemoveAndDisposeImmediately(); } From e2e10a8f267b82baae1e693dcce9d07b847dd569 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 17:14:35 +0900 Subject: [PATCH 13/20] Add some explanatory comments to conditions --- osu.Game/Rulesets/Scoring/HitResult.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index e6aba4a70e..0c898e9543 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool AffectsAccuracy(this HitResult result) { + // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. if (result == HitResult.LegacyComboIncrease) return false; @@ -186,6 +187,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsBasic(this HitResult result) { + // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. if (result == HitResult.LegacyComboIncrease) return false; @@ -250,6 +252,7 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsScorable(this HitResult result) { + // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. if (result == HitResult.LegacyComboIncrease) return true; From 9bca7223f67e09626a0d1fd927f4328ec35248a8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 25 Aug 2022 17:16:30 +0900 Subject: [PATCH 14/20] Adjust xmldoc to better explain score contribution --- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0c898e9543..5047fdea82 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Scoring IgnoreHit, /// - /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not contribute to score. + /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// /// /// DO NOT USE. From 2d2bfac5e9c955b3047dca66c30635492a9d9e9d Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Thu, 25 Aug 2022 17:49:38 +0800 Subject: [PATCH 15/20] used `firstHitObject.Samples` as samples for `mergedHitObject` --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 061c5008c5..2c5bbdb279 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -371,6 +371,7 @@ namespace osu.Game.Rulesets.Osu.Edit Position = firstHitObject.Position, NewCombo = firstHitObject.NewCombo, SampleControlPoint = firstHitObject.SampleControlPoint, + Samples = firstHitObject.Samples, }; if (mergedHitObject.Path.ControlPoints.Count == 0) From 16fee7ac1c402b751a9b0a57b10ba1892841b4de Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Thu, 25 Aug 2022 19:31:47 +0800 Subject: [PATCH 16/20] add visual test --- .../Editor/TestSceneObjectMerging.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index b68231ce64..7a5b6022b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -168,6 +168,99 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); } + [Test] + public void TestSimpleMergeHitSound() + { + HitCircle? circle1 = null; + HitCircle? circle2 = null; + double sliderStartTime = 0; + + AddStep("select first two circles", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(circle2); + sliderStartTime = circle1.StartTime; + }); + + mergeSelection(); + + AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( + (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle2.Position, pathType: null))); + + AddStep("start editor clock", () => + { + EditorClock.Seek(sliderStartTime); + EditorClock.Start(); + }); + + AddStep("stop editor clock", () => + { + EditorClock.Stop(); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + } + + [Test] + public void TestMergeCircleSliderHitsound() + { + HitCircle? circle1 = null; + Slider? slider = null; + HitCircle? circle2 = null; + double sliderStartTime = 0; + + AddStep("select a circle, slider, circle", () => + { + circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); + circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(circle1.StartTime); + EditorBeatmap.SelectedHitObjects.Add(circle1); + EditorBeatmap.SelectedHitObjects.Add(slider); + EditorBeatmap.SelectedHitObjects.Add(circle2); + sliderStartTime = circle1.StartTime; + }); + + mergeSelection(); + + AddAssert("slider created", () => + { + if (circle1 is null || circle2 is null || slider is null) + return false; + + var controlPoints = slider.Path.ControlPoints; + (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; + args[0] = (circle1.Position, PathType.Linear); + + for (int i = 0; i < controlPoints.Count; i++) + { + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + } + + args[^1] = (circle2.Position, null); + return sliderCreatedFor(args); + }); + + AddStep("start editor clock", () => + { + EditorClock.Seek(sliderStartTime); + EditorClock.Start(); + }); + + AddStep("stop editor clock", () => + { + EditorClock.Stop(); + }); + + AddStep("undo", () => Editor.Undo()); + AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + } + private void mergeSelection() { AddStep("merge selection", () => From 78717956d536df4ba1bfea7e37f01920fb1a9f81 Mon Sep 17 00:00:00 2001 From: nanashi-1 Date: Fri, 26 Aug 2022 16:55:18 +0800 Subject: [PATCH 17/20] add visual test --- .../Editor/TestSceneObjectMerging.cs | 88 ++++++------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 7a5b6022b7..75a2361732 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Input; +using System.Collections.Generic; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -169,11 +170,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestSimpleMergeHitSound() + public void TestSimpleMergeSample() { HitCircle? circle1 = null; HitCircle? circle2 = null; - double sliderStartTime = 0; + AddStep("select first two circles", () => { @@ -182,83 +183,31 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor EditorClock.Seek(circle1.StartTime); EditorBeatmap.SelectedHitObjects.Add(circle1); EditorBeatmap.SelectedHitObjects.Add(circle2); - sliderStartTime = circle1.StartTime; }); mergeSelection(); - AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), - (pos: circle2.Position, pathType: null))); - - AddStep("start editor clock", () => - { - EditorClock.Seek(sliderStartTime); - EditorClock.Start(); - }); - - AddStep("stop editor clock", () => - { - EditorClock.Stop(); - }); - - AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + AddAssert("samples exist", () => sliderSampleExist()); } [Test] - public void TestMergeCircleSliderHitsound() + public void TestSliderCircleMergeSample() { - HitCircle? circle1 = null; Slider? slider = null; - HitCircle? circle2 = null; - double sliderStartTime = 0; + HitCircle? circle = null; - AddStep("select a circle, slider, circle", () => + AddStep("select a slider followed by a circle", () => { - circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); - slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > circle1.StartTime); - circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); - EditorClock.Seek(circle1.StartTime); - EditorBeatmap.SelectedHitObjects.Add(circle1); + slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); + circle = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); + EditorClock.Seek(slider.StartTime); EditorBeatmap.SelectedHitObjects.Add(slider); - EditorBeatmap.SelectedHitObjects.Add(circle2); - sliderStartTime = circle1.StartTime; + EditorBeatmap.SelectedHitObjects.Add(circle); }); mergeSelection(); - AddAssert("slider created", () => - { - if (circle1 is null || circle2 is null || slider is null) - return false; - - var controlPoints = slider.Path.ControlPoints; - (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; - args[0] = (circle1.Position, PathType.Linear); - - for (int i = 0; i < controlPoints.Count; i++) - { - args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); - } - - args[^1] = (circle2.Position, null); - return sliderCreatedFor(args); - }); - - AddStep("start editor clock", () => - { - EditorClock.Seek(sliderStartTime); - EditorClock.Start(); - }); - - AddStep("stop editor clock", () => - { - EditorClock.Stop(); - }); - - AddStep("undo", () => Editor.Undo()); - AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && objectsRestored(circle1, circle2)); + AddAssert("samples exist", () => sliderSampleExist()); } private void mergeSelection() @@ -302,5 +251,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return true; } + + private bool sliderSampleExist() + { + if (EditorBeatmap.SelectedHitObjects.Count != 1) + return false; + + var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); + + if (mergedSlider.Samples[0] is null) + return false; + + return true; + } } } From f54047d17b0ae8d50546909e386aeec2d1c0ea1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 18:55:20 +0900 Subject: [PATCH 18/20] Move selection clearing to top --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 14ed305b25..a955a1cce3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -260,6 +260,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (toSplit.Count == 0) return; + editorBeatmap.SelectedHitObjects.Clear(); + foreach (var c in toSplit) { if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) @@ -318,8 +320,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders foreach (var c in controlPoints) c.Position -= first; HitObject.Position += first; - - editorBeatmap.SelectedHitObjects.Clear(); } private void convertToStream() From 47cb163015e9ef4b46e05dbcfc49ec5bbd7bb53e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 19:09:03 +0900 Subject: [PATCH 19/20] Refactor splitting logic and comments slightly --- .../Components/PathControlPointVisualiser.cs | 6 ++--- .../Sliders/SliderSelectionBlueprint.cs | 25 ++++++++----------- 2 files changed, 14 insertions(+), 17 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 48e1d6405d..22cbab8938 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private bool splitSelected() { - List toSplit = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); + List controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList(); // Ensure that there are any points to be split - if (toSplit.Count == 0) + if (controlPointsToSplitAt.Count == 0) return false; changeHandler?.BeginChange(); - SplitControlPointsRequested?.Invoke(toSplit); + SplitControlPointsRequested?.Invoke(controlPointsToSplitAt); changeHandler?.EndChange(); // Since pieces are re-used, they will not point to the deleted control points while remaining selected diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index a955a1cce3..eb69efd636 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -251,34 +251,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Position += first; } - private void splitControlPoints(List toSplit) + private void splitControlPoints(List controlPointsToSplitAt) { // Arbitrary gap in milliseconds to put between split slider pieces const double split_gap = 100; // Ensure that there are any points to be split - if (toSplit.Count == 0) + if (controlPointsToSplitAt.Count == 0) return; editorBeatmap.SelectedHitObjects.Clear(); - foreach (var c in toSplit) + foreach (var splitPoint in controlPointsToSplitAt) { - if (c == controlPoints[0] || c == controlPoints[^1] || c.Type is null) + if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null) continue; // Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider. - var splitControlPoints = controlPoints.TakeWhile(current => current != c).ToList(); + int index = controlPoints.IndexOf(splitPoint); - if (splitControlPoints.Count == 0) + if (index <= 0) continue; - foreach (var current in splitControlPoints) - { - controlPoints.Remove(current); - } - - splitControlPoints.Add(c); + // Extract the split portion and remove from the original slider. + var splitControlPoints = controlPoints.Take(index + 1).ToList(); + controlPoints.RemoveRange(0, index); // Turn the control points which were split off into a new slider. var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone(); @@ -314,8 +311,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position - // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) + // Once all required pieces have been split off, the original slider has the final split. + // As a final step, we must reset its control points to have an origin of (0,0). Vector2 first = controlPoints[0].Position; foreach (var c in controlPoints) c.Position -= first; From cb1c4a1bb106102e0756167ec877fb00d81cc04a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Aug 2022 19:16:12 +0900 Subject: [PATCH 20/20] Move sample checks to be inline in other existing tests --- .../Editor/TestSceneObjectMerging.cs | 53 +++---------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 75a2361732..cdb2a7fe77 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Input; -using System.Collections.Generic; namespace osu.Game.Rulesets.Osu.Tests.Editor { @@ -78,6 +77,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return sliderCreatedFor(args); }); + AddAssert("samples exist", sliderSampleExist); + AddStep("undo", () => Editor.Undo()); AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2)); } @@ -123,6 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return sliderCreatedFor(args); }); + AddAssert("samples exist", sliderSampleExist); + AddAssert("merged slider matches first slider", () => { var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); @@ -166,50 +169,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor (pos: circle1.Position, pathType: PathType.Linear), (pos: circle2.Position, pathType: null))); + AddAssert("samples exist", sliderSampleExist); + AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner)); } - [Test] - public void TestSimpleMergeSample() - { - HitCircle? circle1 = null; - HitCircle? circle2 = null; - - - AddStep("select first two circles", () => - { - circle1 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle); - circle2 = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h != circle1); - EditorClock.Seek(circle1.StartTime); - EditorBeatmap.SelectedHitObjects.Add(circle1); - EditorBeatmap.SelectedHitObjects.Add(circle2); - }); - - mergeSelection(); - - AddAssert("samples exist", () => sliderSampleExist()); - } - - [Test] - public void TestSliderCircleMergeSample() - { - Slider? slider = null; - HitCircle? circle = null; - - AddStep("select a slider followed by a circle", () => - { - slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider); - circle = (HitCircle)EditorBeatmap.HitObjects.First(h => h is HitCircle && h.StartTime > slider.StartTime); - EditorClock.Seek(slider.StartTime); - EditorBeatmap.SelectedHitObjects.Add(slider); - EditorBeatmap.SelectedHitObjects.Add(circle); - }); - - mergeSelection(); - - AddAssert("samples exist", () => sliderSampleExist()); - } - private void mergeSelection() { AddStep("merge selection", () => @@ -259,10 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); - if (mergedSlider.Samples[0] is null) - return false; - - return true; + return mergedSlider.Samples[0] is not null; } } }