mirror of
https://github.com/ppy/osu.git
synced 2025-02-06 04:12:55 +08:00
Merge branch 'master' into no-gameplay-clock-editor-offset
This commit is contained in:
commit
90ff0864c0
@ -77,6 +77,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
return sliderCreatedFor(args);
|
return sliderCreatedFor(args);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddAssert("samples exist", sliderSampleExist);
|
||||||
|
|
||||||
AddStep("undo", () => Editor.Undo());
|
AddStep("undo", () => Editor.Undo());
|
||||||
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
|
AddAssert("merged objects restored", () => circle1 is not null && circle2 is not null && slider is not null && objectsRestored(circle1, slider, circle2));
|
||||||
}
|
}
|
||||||
@ -122,6 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
return sliderCreatedFor(args);
|
return sliderCreatedFor(args);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddAssert("samples exist", sliderSampleExist);
|
||||||
|
|
||||||
AddAssert("merged slider matches first slider", () =>
|
AddAssert("merged slider matches first slider", () =>
|
||||||
{
|
{
|
||||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||||
@ -165,6 +169,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
(pos: circle1.Position, pathType: PathType.Linear),
|
(pos: circle1.Position, pathType: PathType.Linear),
|
||||||
(pos: circle2.Position, pathType: null)));
|
(pos: circle2.Position, pathType: null)));
|
||||||
|
|
||||||
|
AddAssert("samples exist", sliderSampleExist);
|
||||||
|
|
||||||
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
|
AddAssert("spinner not merged", () => EditorBeatmap.HitObjects.Contains(spinner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,5 +215,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool sliderSampleExist()
|
||||||
|
{
|
||||||
|
if (EditorBeatmap.SelectedHitObjects.Count != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||||
|
|
||||||
|
return mergedSlider.Samples[0] is not null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
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();
|
item?.Action?.Value();
|
||||||
});
|
});
|
||||||
|
255
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
Normal file
255
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
// 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.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;
|
||||||
|
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<ComposeBlueprintContainer>().First();
|
||||||
|
|
||||||
|
private Slider? slider;
|
||||||
|
private PathControlPointVisualiser? visualiser;
|
||||||
|
|
||||||
|
private const double split_gap = 100;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicSplit()
|
||||||
|
{
|
||||||
|
double endTime = 0;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
endTime = slider.EndTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select added slider", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||||
|
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||||
|
});
|
||||||
|
|
||||||
|
moveMouseToControlPoint(2);
|
||||||
|
AddStep("select control point", () =>
|
||||||
|
{
|
||||||
|
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, 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 + split_gap,
|
||||||
|
(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 TestDoubleSplit()
|
||||||
|
{
|
||||||
|
double endTime = 0;
|
||||||
|
|
||||||
|
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.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(slider);
|
||||||
|
|
||||||
|
endTime = slider.EndTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("select added slider", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||||
|
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||||
|
});
|
||||||
|
|
||||||
|
moveMouseToControlPoint(2);
|
||||||
|
AddStep("select first control point", () =>
|
||||||
|
{
|
||||||
|
if (visualiser is not null) visualiser.Pieces[2].IsSelected.Value = true;
|
||||||
|
});
|
||||||
|
moveMouseToControlPoint(4);
|
||||||
|
AddStep("select second control point", () =>
|
||||||
|
{
|
||||||
|
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 - 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() + 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() + 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<PathControlPointVisualiser>().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;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
|
|
||||||
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
||||||
|
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IDistanceSnapProvider snapProvider { get; set; }
|
private IDistanceSnapProvider snapProvider { get; set; }
|
||||||
@ -104,6 +105,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool splitSelected()
|
||||||
|
{
|
||||||
|
List<PathControlPoint> controlPointsToSplitAt = Pieces.Where(p => p.IsSelected.Value && isSplittable(p)).Select(p => p.ControlPoint).ToList();
|
||||||
|
|
||||||
|
// Ensure that there are any points to be split
|
||||||
|
if (controlPointsToSplitAt.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
SplitControlPointsRequested?.Invoke(controlPointsToSplitAt);
|
||||||
|
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 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)
|
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
@ -142,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
||||||
{
|
{
|
||||||
Pieces.RemoveAll(p => p.ControlPoint == point);
|
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
|
||||||
Connections.RemoveAll(c => c.ControlPoint == point);
|
piece.RemoveAndDisposeImmediately();
|
||||||
|
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
|
||||||
|
connection.RemoveAndDisposeImmediately();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If removing before the end of the path,
|
// If removing before the end of the path,
|
||||||
@ -322,25 +348,42 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (count == 0)
|
if (count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
List<MenuItem> items = new List<MenuItem>();
|
var splittablePieces = selectedPieces.Where(isSplittable).ToList();
|
||||||
|
int splittableCount = splittablePieces.Count;
|
||||||
|
|
||||||
|
List<MenuItem> curveTypeItems = new List<MenuItem>();
|
||||||
|
|
||||||
if (!selectedPieces.Contains(Pieces[0]))
|
if (!selectedPieces.Contains(Pieces[0]))
|
||||||
items.Add(createMenuItemForPathType(null));
|
curveTypeItems.Add(createMenuItemForPathType(null));
|
||||||
|
|
||||||
// todo: hide/disable items which aren't valid for selected points
|
// todo: hide/disable items which aren't valid for selected points
|
||||||
items.Add(createMenuItemForPathType(PathType.Linear));
|
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
|
||||||
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
||||||
items.Add(createMenuItemForPathType(PathType.Bezier));
|
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
|
||||||
items.Add(createMenuItemForPathType(PathType.Catmull));
|
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
|
||||||
|
|
||||||
return new MenuItem[]
|
var menuItems = new List<MenuItem>
|
||||||
{
|
{
|
||||||
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
|
|
||||||
new OsuMenuItem("Curve type")
|
new OsuMenuItem("Curve type")
|
||||||
{
|
{
|
||||||
Items = items
|
Items = curveTypeItems
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (splittableCount > 0)
|
||||||
|
{
|
||||||
|
menuItems.Add(new OsuMenuItem($"Split {"control point".ToQuantity(splittableCount, splittableCount > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||||
|
MenuItemType.Destructive,
|
||||||
|
() => splitSelected()));
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems.Add(
|
||||||
|
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}",
|
||||||
|
MenuItemType.Destructive,
|
||||||
|
() => DeleteSelected())
|
||||||
|
);
|
||||||
|
|
||||||
|
return menuItems.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
|
|||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@ -111,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
||||||
{
|
{
|
||||||
RemoveControlPointsRequested = removeControlPoints
|
RemoveControlPointsRequested = removeControlPoints,
|
||||||
|
SplitControlPointsRequested = splitControlPoints
|
||||||
});
|
});
|
||||||
|
|
||||||
base.OnSelected();
|
base.OnSelected();
|
||||||
@ -249,6 +251,74 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
HitObject.Position += first;
|
HitObject.Position += first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void splitControlPoints(List<PathControlPoint> 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 (controlPointsToSplitAt.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
editorBeatmap.SelectedHitObjects.Clear();
|
||||||
|
|
||||||
|
foreach (var splitPoint in controlPointsToSplitAt)
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
int index = controlPoints.IndexOf(splitPoint);
|
||||||
|
|
||||||
|
if (index <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
|
||||||
|
|
||||||
|
var newSlider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = HitObject.StartTime,
|
||||||
|
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||||
|
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<HitSampleInfo>)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())
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 += split_gap;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
HitObject.Position += first;
|
||||||
|
}
|
||||||
|
|
||||||
private void convertToStream()
|
private void convertToStream()
|
||||||
{
|
{
|
||||||
if (editorBeatmap == null || beatDivisor == null)
|
if (editorBeatmap == null || beatDivisor == null)
|
||||||
|
@ -371,6 +371,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Position = firstHitObject.Position,
|
Position = firstHitObject.Position,
|
||||||
NewCombo = firstHitObject.NewCombo,
|
NewCombo = firstHitObject.NewCombo,
|
||||||
SampleControlPoint = firstHitObject.SampleControlPoint,
|
SampleControlPoint = firstHitObject.SampleControlPoint,
|
||||||
|
Samples = firstHitObject.Samples,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mergedHitObject.Path.ControlPoints.Count == 0)
|
if (mergedHitObject.Path.ControlPoints.Count == 0)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -314,15 +315,55 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
}), Is.EqualTo(expectedScore).Within(0.5d));
|
}), 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<ArgumentException>(() => scoreProcessor.ApplyBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
|
||||||
|
}));
|
||||||
|
|
||||||
|
ScoreInfo testScore = new ScoreInfo
|
||||||
|
{
|
||||||
|
MaxCombo = 1,
|
||||||
|
Statistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ HitResult.Great, 1 }
|
||||||
|
},
|
||||||
|
MaximumStatistics = new Dictionary<HitResult, int>
|
||||||
|
{
|
||||||
|
{ 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
|
private class TestRuleset : Ruleset
|
||||||
{
|
{
|
||||||
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new System.NotImplementedException();
|
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
||||||
|
|
||||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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 Description => string.Empty;
|
||||||
public override string ShortName => string.Empty;
|
public override string ShortName => string.Empty;
|
||||||
@ -352,5 +393,33 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
|||||||
this.maxResult = maxResult;
|
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<HitResult> GetValidHitResults() => new[] { HitResult.Great };
|
||||||
|
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,8 +119,20 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
[EnumMember(Value = "ignore_hit")]
|
[EnumMember(Value = "ignore_hit")]
|
||||||
[Order(12)]
|
[Order(12)]
|
||||||
IgnoreHit,
|
IgnoreHit,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// DO NOT USE.
|
||||||
|
/// </remarks>
|
||||||
|
[EnumMember(Value = "legacy_combo_increase")]
|
||||||
|
[Order(99)]
|
||||||
|
[Obsolete("Do not use.")]
|
||||||
|
LegacyComboIncrease = 99
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
public static class HitResultExtensions
|
public static class HitResultExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -150,6 +162,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
case HitResult.Perfect:
|
case HitResult.Perfect:
|
||||||
case HitResult.LargeTickHit:
|
case HitResult.LargeTickHit:
|
||||||
case HitResult.LargeTickMiss:
|
case HitResult.LargeTickMiss:
|
||||||
|
case HitResult.LegacyComboIncrease:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -161,13 +174,25 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Whether a <see cref="HitResult"/> affects the accuracy portion of the score.
|
/// Whether a <see cref="HitResult"/> affects the accuracy portion of the score.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool AffectsAccuracy(this HitResult result)
|
public static bool AffectsAccuracy(this HitResult result)
|
||||||
=> IsScorable(result) && !IsBonus(result);
|
{
|
||||||
|
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||||
|
if (result == HitResult.LegacyComboIncrease)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return IsScorable(result) && !IsBonus(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
|
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsBasic(this HitResult result)
|
public static bool IsBasic(this HitResult result)
|
||||||
=> IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
{
|
||||||
|
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||||
|
if (result == HitResult.LegacyComboIncrease)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> should be counted as a tick.
|
/// Whether a <see cref="HitResult"/> should be counted as a tick.
|
||||||
@ -225,12 +250,19 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> is scorable.
|
/// Whether a <see cref="HitResult"/> is scorable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
|
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;
|
||||||
|
|
||||||
|
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An array of all scorable <see cref="HitResult"/>s.
|
/// An array of all scorable <see cref="HitResult"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> is valid within a given <see cref="HitResult"/> range.
|
/// Whether a <see cref="HitResult"/> is valid within a given <see cref="HitResult"/> range.
|
||||||
@ -251,4 +283,5 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
return result > minResult && result < maxResult;
|
return result > minResult && result < maxResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,11 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <param name="result">The <see cref="JudgementResult"/> to apply.</param>
|
/// <param name="result">The <see cref="JudgementResult"/> to apply.</param>
|
||||||
public void ApplyResult(JudgementResult result)
|
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++;
|
JudgedHits++;
|
||||||
lastAppliedResult = result;
|
lastAppliedResult = result;
|
||||||
|
|
||||||
|
@ -536,6 +536,9 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
{
|
{
|
||||||
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
||||||
current.MaxCombo = scoreInfo.MaxCombo;
|
current.MaxCombo = scoreInfo.MaxCombo;
|
||||||
|
|
||||||
|
if (scoreInfo.MaximumStatistics.Count > 0)
|
||||||
|
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -591,7 +594,8 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
|
|
||||||
if (result.IsBonus())
|
if (result.IsBonus())
|
||||||
current.BonusScore += count * Judgement.ToNumericResult(result);
|
current.BonusScore += count * Judgement.ToNumericResult(result);
|
||||||
else
|
|
||||||
|
if (result.AffectsAccuracy())
|
||||||
{
|
{
|
||||||
// The maximum result of this judgement if it wasn't a miss.
|
// 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).
|
// 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).
|
||||||
|
Loading…
Reference in New Issue
Block a user