1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 23:12:56 +08:00

Merge pull request #15583 from bdach/convert-to-stream

Add slider-to-stream conversion operation to osu! editor
This commit is contained in:
Dean Herbert 2021-11-12 13:56:11 +09:00 committed by GitHub
commit a754b40a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 252 additions and 0 deletions

View File

@ -0,0 +1,185 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderStreamConversion : TestSceneOsuEditor
{
private BindableBeatDivisor beatDivisor => (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
[Test]
public void TestSimpleConversion()
{
Slider slider = null;
AddStep("select first slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.25),
(time: 0.5, pathPosition: 0.5),
(time: 0.75, pathPosition: 0.75),
(time: 1, pathPosition: 1)));
AddStep("undo", () => Editor.Undo());
AddAssert("slider restored", () => sliderRestored(slider));
AddStep("select first slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 8);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.125, pathPosition: 0.125),
(time: 0.25, pathPosition: 0.25),
(time: 0.375, pathPosition: 0.375),
(time: 0.5, pathPosition: 0.5),
(time: 0.625, pathPosition: 0.625),
(time: 0.75, pathPosition: 0.75),
(time: 0.875, pathPosition: 0.875),
(time: 1, pathPosition: 1)));
}
[Test]
public void TestConversionWithNonMatchingDivisor()
{
Slider slider = null;
AddStep("select second slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider).ElementAt(1);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 3);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 2 / 3d, pathPosition: 2 / 3d)));
}
[Test]
public void TestConversionWithRepeats()
{
Slider slider = null;
AddStep("select first slider with repeats", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider s && s.RepeatCount > 0);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 2);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.5),
(time: 0.5, pathPosition: 1),
(time: 0.75, pathPosition: 0.5),
(time: 1, pathPosition: 0)));
}
[Test]
public void TestConversionPreservesSliderProperties()
{
Slider slider = null;
AddStep("select second new-combo-starting slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider s && s.NewCombo).ElementAt(1);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.25),
(time: 0.5, pathPosition: 0.5),
(time: 0.75, pathPosition: 0.75),
(time: 1, pathPosition: 1)));
AddStep("undo", () => Editor.Undo());
AddAssert("slider restored", () => sliderRestored(slider));
}
private void convertToStream()
{
AddStep("convert to stream", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.F);
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.LControl);
});
}
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
{
if (EditorBeatmap.HitObjects.Contains(slider))
return false;
foreach ((double expectedTime, double expectedPathPosition) in expectedCircles)
{
double time = slider.StartTime + slider.Duration * expectedTime;
Vector2 position = slider.Position + slider.Path.PositionAt(expectedPathPosition);
if (!EditorBeatmap.HitObjects.OfType<HitCircle>().Any(h => matches(h, time, position, slider.NewCombo && expectedTime == 0)))
return false;
}
return true;
bool matches(HitCircle circle, double time, Vector2 position, bool startsNewCombo) =>
Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
}
private bool sliderRestored(Slider slider)
{
var objects = EditorBeatmap.HitObjects.Where(h => h.StartTime >= slider.StartTime && h.GetEndTime() <= slider.EndTime).ToList();
if (objects.Count > 1)
return false;
var hitObject = objects.Single();
if (!(hitObject is Slider restoredSlider))
return false;
return Precision.AlmostEquals(slider.StartTime, restoredSlider.StartTime)
&& Precision.AlmostEquals(slider.GetEndTime(), restoredSlider.GetEndTime())
&& Precision.AlmostEquals(slider.Position, restoredSlider.Position, 0.01f)
&& Precision.AlmostEquals(slider.EndPosition, restoredSlider.EndPosition, 0.01f);
}
}
}

View File

@ -11,9 +11,12 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
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.Rulesets.Osu.Objects.Drawables;
@ -47,6 +50,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad;
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
@ -173,6 +179,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (!IsSelected)
return false;
if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed)
{
convertToStream();
return true;
}
return false;
}
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
@ -234,9 +254,56 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap?.Update(HitObject);
}
private void convertToStream()
{
if (editorBeatmap == null || changeHandler == null || beatDivisor == null)
return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler.BeginChange();
int i = 0;
double time = HitObject.StartTime;
while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1))
{
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
// and indicates how many fractional spans of a slider have passed up to time.
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position.
if (Precision.AlmostBigger(positionWithRepeats % 2, 1))
pathPosition = 1 - pathPosition;
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
samplePoint.Time = time;
editorBeatmap.Add(new HitCircle
{
StartTime = time,
Position = position,
NewCombo = i == 0 && HitObject.NewCombo,
SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
});
i += 1;
time = HitObject.StartTime + i * streamSpacing;
}
editorBeatmap.Remove(HitObject);
changeHandler.EndChange();
}
public override MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
};
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.