1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge branch 'master' into login-state-changes-user-profile-overlay

This commit is contained in:
Dean Herbert 2023-11-22 12:13:49 +09:00 committed by GitHub
commit d86e743745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1444 additions and 448 deletions

View File

@ -108,6 +108,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Setup JDK 11
uses: actions/setup-java@v3
with:
distribution: microsoft
java-version: 11
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v3
with:

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1111.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1121.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("update hit object path", () =>
{
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
hitObject.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(100, 100),
@ -190,16 +190,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
[Test]
public void TestVertexResampling()
{
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
addBlueprintStep(100, 100, new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(100, 100),
new Vector2(50, 200),
}), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
addAddVertexSteps(150, 150);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR);
}
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1;
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER;
} while (rng.Next(3) != 0);
if (rng.Next(5) == 0)
@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.Tests
foreach (var point in sliderPath.ControlPoints)
{
Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null);
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
var stream = new JuiceStream
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(100, 0),

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
X = CatchPlayfield.CENTER_X,
StartTime = 3000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 })
}
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(width, 0)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStream
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, -192) }),
X = CatchPlayfield.WIDTH / 2
}
}

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
X = xCoords,
StartTime = playfieldTime + 1000,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 200)

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStream
{
X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 100)

View File

@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.LINEAR))
{
path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.

View File

@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Catch.Objects
for (int i = 1; i < vertices.Count; i++)
{
sliderPath.ControlPoints[^1].Type = PathType.Linear;
sliderPath.ControlPoints[^1].Type = PathType.LINEAR;
float deltaX = vertices[i].X - lastPosition.X;
double length = (vertices[i].Time - currentTime) * velocity;

View File

@ -0,0 +1,94 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Test]
public void TestNormalSelection()
{
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
));
addReset();
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
));
addReset();
AddStep("add notes to row", () =>
{
if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3))
return;
ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0);
ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime };
ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime };
ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime };
EditorBeatmap.AddRange(new[] { second, third, forth });
});
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
));
addReset();
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
));
}
[Test]
public void TestUnusualSelection()
{
addStepClickLink("00:00:000 (0|1)", "wrong offset");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
addReset();
addStepClickLink("00:00:000 (2)", "std link");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
addReset();
addStepClickLink("00:00:000 (1,2)", "std link");
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
}
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
{
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
}
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
{
bool checkColumns = columnPairs != null
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
: !EditorBeatmap.SelectedHitObjects.Any();
return EditorClock.CurrentTime == startTime
&& EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0)
&& checkColumns;
}
private bool isNoteAt(HitObject hitObject, double time, int column) =>
hitObject is ManiaHitObject maniaHitObject
&& maniaHitObject.StartTime == time
&& maniaHitObject.Column == column;
}
}

View File

@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -50,5 +51,37 @@ namespace osu.Game.Rulesets.Mania.Edit
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
// 123|0,456|1,789|2 ...
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] objectDescriptions = objectDescription.Split(',').ToArray();
for (int i = 0; i < objectDescriptions.Length; i++)
{
string[] split = objectDescriptions[i].Split('|').ToArray();
if (split.Length != 2)
continue;
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < objectDescriptions.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
}
}

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(420, 240),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(-100, 0))
}),
}
@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
}
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
StackHeight = 5
@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = new Vector2(0, 0),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(playfield_centre)
}),
}
@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(-playfield_centre)
}),
}
@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
Path = new SliderPath(new[]
{
// Circular arc shoots over the top of the screen.
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(-100, -200)),
new PathControlPoint(new Vector2(100, -200))
}),

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
AddStep("undo", () => Editor.Undo());
@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var controlPoints = slider.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2];
args[0] = (circle1.Position, PathType.Linear);
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[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.LINEAR : controlPoints[i].Type);
}
args[^1] = (circle2.Position, null);
@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
AddAssert("samples exist", sliderSampleExist);
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
mergeSelection();
AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor(
(pos: circle1.Position, pathType: PathType.Linear),
(pos: circle1.Position, pathType: PathType.LINEAR),
(pos: circle2.Position, pathType: null)));
}

View File

@ -0,0 +1,91 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneOpenEditorTimestampInOsu : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestNormalSelection()
{
addStepClickLink("00:02:170 (1,2,3)");
checkSelection(() => 2_170, 1, 2, 3);
addReset();
addStepClickLink("00:04:748 (2,3,4,1,2)");
checkSelection(() => 4_748, 2, 3, 4, 1, 2);
addReset();
addStepClickLink("00:02:170 (1,1,1)");
checkSelection(() => 2_170, 1, 1, 1);
addReset();
addStepClickLink("00:02:873 (2,2,2,2)");
checkSelection(() => 2_873, 2, 2, 2, 2);
}
[Test]
public void TestUnusualSelection()
{
HitObject firstObject = null!;
AddStep("retrieve first object", () => firstObject = EditorBeatmap.HitObjects.First());
addStepClickLink("00:00:000 (0)", "invalid combo");
checkSelection(() => firstObject.StartTime);
addReset();
addStepClickLink("00:00:000 (1)", "wrong offset");
checkSelection(() => firstObject.StartTime, 1);
addReset();
addStepClickLink("00:00:956 (2,3,4)", "wrong offset");
checkSelection(() => firstObject.StartTime, 2, 3, 4);
addReset();
addStepClickLink("00:00:956 (956|1,956|2)", "mania link");
checkSelection(() => firstObject.StartTime);
}
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
{
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
}
private void checkSelection(Func<double> startTime, params int[] comboNumbers)
=> AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () =>
{
bool checkCombos = comboNumbers.Any()
? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers)
: !EditorBeatmap.SelectedHitObjects.Any();
return EditorClock.CurrentTime == startTime()
&& EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length
&& checkCombos;
});
private bool hasCombosInOrder(IEnumerable<HitObject> selected, params int[] comboNumbers)
{
List<HitObject> hitObjects = selected.ToList();
if (hitObjects.Count != comboNumbers.Length)
return false;
return !hitObjects.Select(x => (OsuHitObject)x)
.Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i])
.Any();
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(-100, 0)),
new PathControlPoint(new Vector2(100, 20))
};

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -63,9 +63,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(1, PathType.PerfectCurve);
assertControlPointPathType(3, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(1, PathType.PERFECT_CURVE);
assertControlPointPathType(3, PathType.BEZIER);
}
[Test]
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -83,8 +83,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(2, PathType.PerfectCurve);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(2, PathType.PERFECT_CURVE);
assertControlPointPathType(4, null);
}
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
}
@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Linear);
addControlPointStep(new Vector2(200), PathType.LINEAR);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
addContextMenuItemStep("Perfect curve");
assertControlPointPathType(0, PathType.Linear);
assertControlPointPathType(1, PathType.PerfectCurve);
assertControlPointPathType(3, PathType.Linear);
assertControlPointPathType(0, PathType.LINEAR);
assertControlPointPathType(1, PathType.PERFECT_CURVE);
assertControlPointPathType(3, PathType.LINEAR);
}
[Test]
@ -133,21 +133,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.Bezier);
addControlPointStep(new Vector2(300), PathType.PerfectCurve);
addControlPointStep(new Vector2(200), PathType.BEZIER);
addControlPointStep(new Vector2(300), PathType.PERFECT_CURVE);
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200), PathType.Bezier);
addControlPointStep(new Vector2(700, 200), PathType.BEZIER);
addControlPointStep(new Vector2(500, 100));
moveMouseToControlPoint(3);
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
addContextMenuItemStep("Inherit");
assertControlPointPathType(0, PathType.Bezier);
assertControlPointPathType(1, PathType.Bezier);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(1, PathType.BEZIER);
assertControlPointPathType(3, null);
}
[Test]
public void TestCatmullAvailableIffSelectionContainsCatmull()
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.CATMULL);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
addControlPointStep(new Vector2(500, 100));
moveMouseToControlPoint(2);
AddStep("select first and third control point", () =>
{
visualiser.Pieces[0].IsSelected.Value = true;
visualiser.Pieces[2].IsSelected.Value = true;
});
addContextMenuItemStep("Catmull");
assertControlPointPathType(0, PathType.CATMULL);
assertControlPointPathType(2, PathType.CATMULL);
assertControlPointPathType(4, null);
}
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{
Anchor = Anchor.Centre,
@ -158,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void addControlPointStep(Vector2 position, PathType? type)
{
AddStep($"add {type} control point at {position}", () =>
AddStep($"add {type?.Type} control point at {position}", () =>
{
slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
});

View File

@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(256, 192),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
assertControlPointPosition(2, new Vector2(450, 50));
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(2, PathType.PERFECT_CURVE);
assertControlPointPosition(3, new Vector2(550, 50));
@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50)));
assertControlPointPosition(0, Vector2.Zero);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointPosition(1, new Vector2(0, 100));
@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -282,13 +282,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(400, 0.01f));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
addMovementStep(new Vector2(150, 50));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(1, new Vector2(150, 50));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -298,32 +298,32 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(350, 0.01f));
assertControlPointType(2, PathType.Bezier);
assertControlPointType(2, PathType.BEZIER);
addMovementStep(new Vector2(150, 150));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(150, 150));
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestDragControlPointPathAfterChangingType()
{
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier);
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.BEZIER);
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve);
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECT_CURVE);
moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
assertControlPointType(3, PathType.PerfectCurve);
assertControlPointType(3, PathType.PERFECT_CURVE);
addMovementStep(new Vector2(350, 0.01f));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(4, new Vector2(350, 0.01f));
assertControlPointType(3, PathType.Bezier);
assertControlPointType(3, PathType.BEZIER);
}
private void addMovementStep(Vector2 relativePosition)

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};
@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(0, 10))
};
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(0, 50)),
new PathControlPoint(new Vector2(0, 100))
};

View File

@ -1,8 +1,7 @@
// 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.
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
}
[Test]
@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
}
[Test]
@ -90,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -112,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -131,8 +130,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
}
[Test]
@ -150,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertLength(100);
}
@ -172,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -196,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(4);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -216,8 +215,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.Linear);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR);
}
[Test]
@ -240,8 +239,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.Linear);
assertControlPointType(1, PathType.PerfectCurve);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE);
}
[Test]
@ -269,25 +268,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestBeginPlacementWithoutReleasingMouse()
public void TestSliderDrawingDoesntActivateAfterNormalPlacement()
{
Vector2 startPoint = new Vector2(200);
addMovementStep(startPoint);
addClickStep(MouseButton.Left);
for (int i = 0; i < 20; i++)
{
if (i == 5)
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
}
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(false);
addClickStep(MouseButton.Right);
assertPlaced(true);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
public void TestSliderDrawingCurve()
{
Vector2 startPoint = new Vector2(200);
addMovementStep(startPoint);
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
for (int i = 0; i < 20; i++)
addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50));
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
assertPlaced(true);
assertLength(760, tolerance: 10);
assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(3));
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
assertControlPointType(4, null);
}
[Test]
public void TestSliderDrawingLinear()
{
addMovementStep(new Vector2(200));
AddStep("press left button", () => InputManager.PressButton(MouseButton.Left));
addMovementStep(new Vector2(300, 200));
addMovementStep(new Vector2(400, 200));
addMovementStep(new Vector2(400, 300));
addMovementStep(new Vector2(400));
addMovementStep(new Vector2(300, 400));
addMovementStep(new Vector2(200, 400));
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertLength(200);
assertControlPointCount(2);
assertControlPointType(0, PathType.Linear);
assertLength(600, tolerance: 10);
assertControlPointCount(4);
assertControlPointType(0, PathType.LINEAR);
assertControlPointType(1, null);
assertControlPointType(2, null);
assertControlPointType(3, null);
}
[Test]
@ -306,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -326,7 +379,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -347,7 +400,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
[Test]
@ -368,7 +421,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.Bezier);
assertControlPointType(0, PathType.BEZIER);
}
[Test]
@ -385,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true);
assertControlPointCount(3);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointType(0, PathType.PERFECT_CURVE);
}
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@ -397,16 +450,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected);
private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1));
private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider()!.Distance, () => Is.EqualTo(expected).Within(tolerance));
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type);
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1));
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));
private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();

View File

@ -22,12 +22,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private readonly PathControlPoint[][] paths =
{
createPathSegment(
PathType.PerfectCurve,
PathType.PERFECT_CURVE,
new Vector2(200, -50),
new Vector2(250, 0)
),
createPathSegment(
PathType.Linear,
PathType.LINEAR,
new Vector2(100, 0),
new Vector2(100, 100)
)

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider = new Slider
{
Position = new Vector2(256, 192),
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(150, 150),

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
ControlPoints =
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(136, 205)),
new PathControlPoint(new Vector2(-4, 226))
}
@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("rotate 90 degrees ccw", () =>
@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
selectionHandler.HandleRotation(-90);
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
}
[Test]
@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
OsuSelectionHandler selectionHandler;
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("flip slider horizontally", () =>
@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
});
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
}
[Test]

View File

@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})
@ -73,20 +73,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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(0, 50), PathType.PERFECT_CURVE),
(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(300, 50), PathType.PERFECT_CURVE),
(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(0, 50), PathType.PERFECT_CURVE),
(new Vector2(150, 200), null),
(new Vector2(300, 50), PathType.PerfectCurve),
(new Vector2(300, 50), PathType.PERFECT_CURVE),
(new Vector2(400, 50), null),
(new Vector2(400, 200), null)
));
@ -104,11 +104,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.Bezier),
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(400, 150), PathType.CATMULL),
new PathControlPoint(new Vector2(300, 200)),
new PathControlPoint(new Vector2(400, 250))
})
@ -139,15 +139,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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(0, 50), PathType.PERFECT_CURVE),
(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(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(400, 200), PathType.CATMULL),
(new Vector2(300, 250), null),
(new Vector2(400, 300), null)
));
@ -165,9 +165,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Position = new Vector2(0, 50),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(150, 150)),
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE),
new PathControlPoint(new Vector2(400, 0)),
new PathControlPoint(new Vector2(400, 150))
})

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0), PathType.LINEAR),
new PathControlPoint(new Vector2(100, 0)),
};

View File

@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new Slider
{
StartTime = 3200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 5200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
}
}
},
@ -105,12 +105,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new Slider
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 4000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
},
}
},
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
StartTime = 3000,
Position = new Vector2(156, 242),
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
},
new Spinner
{

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
var slider = new Slider
{
StartTime = 1000,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
};
CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss);

View File

@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
ControlPoints =
{
new PathControlPoint(new Vector2(), PathType.Linear),
new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0)
new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128)
new PathControlPoint(new Vector2(), PathType.LINEAR),
new PathControlPoint(new Vector2(-64, -128), PathType.LINEAR), // absolute position: (64, 0)
new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128)
}
},
RepeatCount = 1

View File

@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 500,
Position = new Vector2(250),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(0, 100),

View File

@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(50, 0),
@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(50, 0),
@ -391,7 +391,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -428,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -438,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -521,7 +521,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -531,7 +531,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -571,7 +571,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -581,7 +581,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),

View File

@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(154, 28),
@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Tests
SliderVelocityMultiplier = speedMultiplier,
StartTime = Time.Current + time_offset,
Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(0, distance),
@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(max_length / 2, max_length / 2),
@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.375f, max_length * 0.18f),
@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.375f, max_length * 0.18f),
@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(-max_length / 2, 0),
@ -365,7 +365,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[]
Path = new SliderPath(PathType.CATMULL, new[]
{
Vector2.Zero,
new Vector2(max_length * 0.125f, max_length * 0.125f),

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(150, 100),
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
ComboIndex = 1,
StartTime = dho.HitObject.StartTime,
Path = new SliderPath(PathType.Bezier, new[]
Path = new SliderPath(PathType.BEZIER, new[]
{
Vector2.Zero,
new Vector2(150, 100),
@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(150, 100),

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = velocity,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(followCircleRadius, 0),

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(slider_path_length * 10, 0),
@ -488,7 +488,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(slider_path_length, 0),

View File

@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 3000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 13000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = 23000,
Position = new Vector2(100, 100),
Path = new SliderPath(PathType.PerfectCurve, new[]
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(300, 200)

View File

@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -318,7 +318,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -352,7 +352,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),
@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(25, 0),

View File

@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
if (ShouldSerializeFlashlightRating())
if (ShouldSerializeFlashlightDifficulty())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// unless the fields are also renamed.
[UsedImplicitly]
public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight);
public bool ShouldSerializeFlashlightDifficulty() => Mods.Any(m => m is ModFlashlight);
#endregion
}

View File

@ -221,11 +221,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
private void updatePathType()
{
if (ControlPoint.Type != PathType.PerfectCurve)
if (ControlPoint.Type != PathType.PERFECT_CURVE)
return;
if (PointsInSegment.Count > 3)
ControlPoint.Type = PathType.Bezier;
ControlPoint.Type = PathType.BEZIER;
if (PointsInSegment.Count != 3)
return;
@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type = PathType.Bezier;
ControlPoint.Type = PathType.BEZIER;
}
/// <summary>
@ -256,18 +256,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Color4 getColourFromNodeType()
{
if (!(ControlPoint.Type is PathType pathType))
if (ControlPoint.Type is not PathType pathType)
return colours.Yellow;
switch (pathType)
switch (pathType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
return colours.SeaFoam;
case PathType.Bezier:
return colours.Pink;
case SplineType.BSpline:
if (!pathType.Degree.HasValue)
return colours.PinkLighter;
case PathType.PerfectCurve:
int idx = Math.Clamp(pathType.Degree.Value, 0, 3);
return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx];
case SplineType.PerfectCurve:
return colours.PurpleDark;
default:
@ -275,6 +279,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty;
public LocalisableString TooltipText => ControlPoint.Type?.Description ?? string.Empty;
}
}

View File

@ -242,18 +242,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
switch (type)
if (type?.Type == SplineType.PerfectCurve)
{
case PathType.PerfectCurve:
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
// Can't always create a circular arc out of 4 or more points,
// so we split the segment into one 3-point circular arc segment
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
break;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
}
hitObject.Path.ExpectedDistance.Value = null;
@ -367,13 +364,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
List<MenuItem> curveTypeItems = new List<MenuItem>();
if (!selectedPieces.Contains(Pieces[0]))
{
curveTypeItems.Add(createMenuItemForPathType(null));
curveTypeItems.Add(new OsuMenuItemSpacer());
}
// todo: hide/disable items which aren't valid for selected points
curveTypeItems.Add(createMenuItemForPathType(PathType.Linear));
curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve));
curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier));
curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull));
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
var menuItems = new List<MenuItem>
{
@ -405,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type);

View File

@ -3,6 +3,7 @@
#nullable disable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
@ -10,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -44,6 +46,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
public SliderPlacementBlueprint()
@ -51,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
RelativeSizeAxes = Axes.Both;
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear));
HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.LINEAR));
currentSegmentLength = 1;
}
@ -66,13 +73,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false)
};
setState(SliderPlacementState.Initial);
state = SliderPlacementState.Initial;
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
if (freehandToolboxGroup != null)
{
freehandToolboxGroup.Tolerance.BindValueChanged(e =>
{
bSplineBuilder.Tolerance = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
freehandToolboxGroup.CornerThreshold.BindValueChanged(e =>
{
bSplineBuilder.CornerThreshold = e.NewValue;
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}, true);
}
}
[Resolved]
@ -87,8 +109,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
double? nearestSliderVelocity = (editorBeatmap
.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier;
HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
@ -98,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
ApplyDefaultsToHitObject();
break;
case SliderPlacementState.Body:
case SliderPlacementState.ControlPoints:
updateCursor();
break;
}
@ -115,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
beginCurve();
break;
case SliderPlacementState.Body:
case SliderPlacementState.ControlPoints:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
@ -128,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Debug.Assert(lastPoint != null);
segmentStart = lastPoint;
segmentStart.Type = PathType.Linear;
segmentStart.Type = PathType.LINEAR;
currentSegmentLength = 1;
}
@ -139,25 +162,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button != MouseButton.Left)
return base.OnDragStart(e);
if (state != SliderPlacementState.ControlPoints)
return base.OnDragStart(e);
// Only enter drawing mode if no additional control points have been placed.
int controlPointCount = HitObject.Path.ControlPoints.Count;
if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor))
return base.OnDragStart(e);
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position);
state = SliderPlacementState.Drawing;
return true;
}
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
if (state == SliderPlacementState.Drawing)
{
bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position);
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
}
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
if (state == SliderPlacementState.Drawing)
endCurve();
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (state == SliderPlacementState.Body && e.Button == MouseButton.Right)
if (state == SliderPlacementState.ControlPoints && e.Button == MouseButton.Right)
endCurve();
base.OnMouseUp(e);
}
private void beginCurve()
{
BeginPlacement(commitStart: true);
setState(SliderPlacementState.Body);
}
private void endCurve()
{
updateSlider();
EndPlacement(true);
}
protected override void Update()
{
base.Update();
@ -167,21 +215,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updatePathType();
}
private void beginCurve()
{
BeginPlacement(commitStart: true);
state = SliderPlacementState.ControlPoints;
}
private void endCurve()
{
updateSlider();
EndPlacement(true);
}
private void updatePathType()
{
if (state == SliderPlacementState.Drawing)
{
segmentStart.Type = PathType.BSpline(3);
return;
}
switch (currentSegmentLength)
{
case 1:
case 2:
segmentStart.Type = PathType.Linear;
segmentStart.Type = PathType.LINEAR;
break;
case 3:
segmentStart.Type = PathType.PerfectCurve;
segmentStart.Type = PathType.PERFECT_CURVE;
break;
default:
segmentStart.Type = PathType.Bezier;
segmentStart.Type = PathType.BEZIER;
break;
}
}
@ -195,13 +261,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero });
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
// The path type should be adjusted in the progression of updatePathType() (LINEAR -> PC -> BEZIER).
currentSegmentLength++;
updatePathType();
}
// Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
@ -210,7 +276,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Path.ControlPoints.Remove(cursor);
cursor = null;
// The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear).
// The path type should be adjusted in the reverse progression of updatePathType() (BEZIER -> PC -> LINEAR).
currentSegmentLength--;
updatePathType();
}
@ -240,15 +306,55 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
tailCirclePiece.UpdateFrom(HitObject.TailCircle);
}
private void setState(SliderPlacementState newState)
private void updateSliderPathFromBSplineBuilder()
{
state = newState;
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
if (builderPoints.Count == 0)
return;
int lastSegmentStart = 0;
PathType? lastPathType = null;
HitObject.Path.ControlPoints.Clear();
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
for (int i = 0; i < builderPoints.Count; i++)
{
bool isLastPoint = i == builderPoints.Count - 1;
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
if (isNewSegment || isLastPoint)
{
int pointsInSegment = i - lastSegmentStart;
// Where possible, we can use the simpler LINEAR path type.
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
pathType = null;
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
for (int j = lastSegmentStart + 1; j < i; j++)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
if (isLastPoint)
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
lastSegmentStart = (i += 2);
if (pathType != null) lastPathType = pathType;
}
}
}
private enum SliderPlacementState
{
Initial,
Body,
ControlPoints,
Drawing
}
}
}

View File

@ -0,0 +1,100 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class FreehandSliderToolboxGroup : EditorToolboxGroup
{
public FreehandSliderToolboxGroup()
: base("slider")
{
}
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
{
MinValue = 0.05f,
MaxValue = 3f,
Precision = 0.01f
};
public BindableFloat CornerThreshold { get; } = new BindableFloat(0.4f)
{
MinValue = 0.05f,
MaxValue = 1f,
Precision = 0.01f
};
// We map internal ranges to a more standard range of values for display to the user.
private readonly BindableInt displayTolerance = new BindableInt(40)
{
MinValue = 5,
MaxValue = 100
};
private readonly BindableInt displayCornerThreshold = new BindableInt(40)
{
MinValue = 5,
MaxValue = 100
};
private ExpandableSlider<int> toleranceSlider = null!;
private ExpandableSlider<int> cornerThresholdSlider = null!;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
toleranceSlider = new ExpandableSlider<int>
{
Current = displayTolerance
},
cornerThresholdSlider = new ExpandableSlider<int>
{
Current = displayCornerThreshold
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
displayTolerance.BindValueChanged(tolerance =>
{
toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}";
toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}";
Tolerance.Value = displayToInternalTolerance(tolerance.NewValue);
}, true);
displayCornerThreshold.BindValueChanged(threshold =>
{
cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}";
cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}";
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
}, true);
Tolerance.BindValueChanged(tolerance =>
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
);
CornerThreshold.BindValueChanged(threshold =>
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
);
float displayToInternalTolerance(float v) => v / 33f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
float displayToInternalCornerThreshold(float v) => v / 100f;
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
}
}
}

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@ -63,6 +64,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
[BackgroundDependencyLoader]
private void load()
{
@ -94,10 +98,12 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
RightToolbox.AddRange(new EditorToolboxGroup[]
{
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, },
FreehandlSliderToolboxGroup
}
);
}
protected override ComposeBlueprintContainer CreateBlueprintContainer()
@ -106,6 +112,34 @@ namespace osu.Game.Rulesets.Osu.Edit
public override string ConvertSelectionToString()
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
public override void SelectFromTimestamp(double timestamp, string objectDescription)
{
if (!selection_regex.IsMatch(objectDescription))
return;
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',').ToArray();
for (int i = 0; i < splitDescription.Length; i++)
{
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
continue;
OsuHitObject current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
if (current == null)
continue;
EditorBeatmap.SelectedHitObjects.Add(current);
if (i < splitDescription.Length - 1)
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
}
}
private DistanceSnapGrid distanceSnapGrid;
private Container distanceSnapGridContainer;

View File

@ -321,7 +321,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear));
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR));
}
// Merge all the selected hit objects into one slider path.
@ -351,7 +351,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear;
mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR;
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));

View File

@ -339,6 +339,11 @@ namespace osu.Game.Rulesets.Osu.Replays
AddFrameToReplay(startFrame);
// 0.05 rad/ms, or ~477 RPM, as per stable.
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
switch (h)
{
// We add intermediate frames for spinning / following a slider here.
@ -354,7 +359,7 @@ namespace osu.Game.Rulesets.Osu.Replays
for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{
t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
angle += (float)t / 20;
angle += (float)t * radsPerMillisecond;
Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
@ -363,7 +368,7 @@ namespace osu.Game.Rulesets.Osu.Replays
}
t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
angle += (float)t / 20;
angle += (float)t * radsPerMillisecond;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);

View File

@ -88,9 +88,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f)
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
}
protected override void OnSliderBreak()

View File

@ -59,9 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f)
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
}
protected override void OnSliderBreak()

View File

@ -44,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void OnSliderTick()
{
this.ScaleTo(2.2f)
.ScaleTo(2f, 200);
if (Scale.X >= 2f)
{
this.ScaleTo(2.2f)
.ScaleTo(2f, 200);
}
}
protected override void OnSliderBreak()

View File

@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
=> new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion
}

View File

@ -663,7 +663,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL);
}
void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
static void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null)
{
Assert.AreEqual(normalBank, hitObject.Samples[0].Bank);
@ -808,14 +808,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
var first = ((IHasPath)decoded.HitObjects[0]).Path;
Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE));
Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null));
// ReSharper disable once HeuristicUnreachableCode
// weird one, see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier));
Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15)));
Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null));
Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132)));
@ -827,7 +827,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var second = ((IHasPath)decoded.HitObjects[1]).Path;
Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE));
Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
@ -837,14 +837,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
var third = ((IHasPath)decoded.HitObjects[2]).Path;
Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192)));
Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192)));
Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0)));
Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192)));
Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192)));
@ -856,7 +856,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
@ -870,7 +870,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
@ -881,7 +881,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4)));
Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.BEZIER));
Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5)));
Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null));
@ -889,12 +889,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[0].Type == PathType.BEZIER);
Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(sixth.ControlPoints[1].Type == null);
Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[2].Type == PathType.BEZIER);
Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(sixth.ControlPoints[3].Type == null);
Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
@ -904,12 +904,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECT_CURVE);
Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(seventh.ControlPoints[1].Type == null);
Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECT_CURVE);
Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(seventh.ControlPoints[3].Type == null);
Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
@ -1016,7 +1016,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
Assert.That(controlPoints.Count, Is.EqualTo(6));
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull));
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.CATMULL));
}
}
@ -1032,9 +1032,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
Assert.That(controlPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull));
Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull));
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL));
Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.CATMULL));
Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.CATMULL));
Assert.That(controlPoints[3].Type, Is.Null);
}
}
@ -1051,7 +1051,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
Assert.That(controlPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL));
Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(controlPoints[1].Type, Is.Null);
Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));

View File

@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
static ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
@ -113,6 +113,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
}
[Test]
public void TestEncodeBSplineCurveType()
{
var beatmap = new Beatmap
{
HitObjects =
{
new Slider
{
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.BSpline(3)),
new PathControlPoint(new Vector2(50)),
new PathControlPoint(new Vector2(100), PathType.BSpline(3)),
new PathControlPoint(new Vector2(150))
})
},
}
};
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty);
var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0];
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4));
Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3)));
Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3)));
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@ -125,10 +152,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Position = new Vector2(0.6f),
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(Vector2.Zero, PathType.BEZIER),
new PathControlPoint(new Vector2(0.5f)),
new PathControlPoint(new Vector2(0.51f)), // This is actually on the same position as the previous one in legacy beatmaps (truncated to int).
new PathControlPoint(new Vector2(1f), PathType.Bezier),
new PathControlPoint(new Vector2(1f), PathType.BEZIER),
new PathControlPoint(new Vector2(2f))
})
},

View File

@ -162,7 +162,7 @@ namespace osu.Game.Tests.Editing
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(Vector2.One),
new PathControlPoint(new Vector2(2), PathType.Bezier),
new PathControlPoint(new Vector2(2), PathType.BEZIER),
new PathControlPoint(new Vector2(3)),
}, 50)
},
@ -179,7 +179,7 @@ namespace osu.Game.Tests.Editing
StartTime = 2000,
Path = new SliderPath(new[]
{
new PathControlPoint(Vector2.Zero, PathType.Bezier),
new PathControlPoint(Vector2.Zero, PathType.BEZIER),
new PathControlPoint(new Vector2(4)),
new PathControlPoint(new Vector2(5)),
}, 100)

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100, 0), PathType.Bezier)
new PathControlPoint(new Vector2(100, 0), PathType.BEZIER)
}
}
};

View File

@ -34,51 +34,51 @@ namespace osu.Game.Tests.Visual.Editing
{
new MenuItem("File")
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem("Clear All Notes"),
new EditorMenuItem("Open Difficulty..."),
new EditorMenuItem("Save"),
new EditorMenuItem("Create a new Difficulty..."),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Revert to Saved"),
new EditorMenuItem("Revert to Saved (Full)"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Test Beatmap"),
new EditorMenuItem("Open AiMod"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Upload Beatmap..."),
new EditorMenuItem("Export Package"),
new EditorMenuItem("Export Map Package"),
new EditorMenuItem("Import from..."),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Open Song Folder"),
new EditorMenuItem("Open .osu in Notepad"),
new EditorMenuItem("Open .osb in Notepad"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Exit"),
}
},
new MenuItem("Timing")
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem("Time Signature"),
new EditorMenuItem("Metronome Clicks"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Add Timing Section"),
new EditorMenuItem("Add Inheriting Section"),
new EditorMenuItem("Reset Current Section"),
new EditorMenuItem("Delete Timing Section"),
new EditorMenuItem("Resnap Current Section"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Timing Setup"),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive),
new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive),
new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive),
new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem("Set Current Position as Preview Point"),
}
},

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Editing
new Slider
{
Position = new Vector2(128, 256),
Path = new SliderPath(PathType.Linear, new[]
Path = new SliderPath(PathType.LINEAR, new[]
{
Vector2.Zero,
new Vector2(216, 0),

View File

@ -0,0 +1,151 @@
// 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.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene
{
private Editor editor => (Editor)Game.ScreenStack.CurrentScreen;
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().Single();
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().Single();
[Test]
public void TestErrorNotifications()
{
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
addStepClickLink("00:00:000", waitForSeek: false);
AddAssert("received 'must be in edit'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
() => Is.EqualTo(1));
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
addStepClickLink("00:00:000 (1)", waitForSeek: false);
AddAssert("received 'must be in edit'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
() => Is.EqualTo(2));
setUpEditor(rulesetInfo);
AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
addStepClickLink("00:000", "invalid link", waitForSeek: false);
AddAssert("received 'failed to process'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
() => Is.EqualTo(1));
addStepClickLink("50000:00:000", "too long link", waitForSeek: false);
AddAssert("received 'failed to process'",
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
() => Is.EqualTo(2));
}
[Test]
public void TestHandleCurrentScreenChanges()
{
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
setUpEditor(rulesetInfo);
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
addStepClickLink("100:00:000", "long link");
AddUntilStep("moved to end of track", () => editorClock.CurrentTime, () => Is.EqualTo(editorClock.TrackLength));
addStepScreenModeTo(EditorScreenMode.SongSetup);
addStepClickLink("00:00:000");
assertOnScreenAt(EditorScreenMode.SongSetup, 0);
addStepClickLink("00:05:000 (0|0)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Design);
addStepClickLink("00:10:000");
assertOnScreenAt(EditorScreenMode.Design, 10_000);
addStepClickLink("00:15:000 (1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Timing);
addStepClickLink("00:20:000");
assertOnScreenAt(EditorScreenMode.Timing, 20_000);
addStepClickLink("00:25:000 (0,1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepScreenModeTo(EditorScreenMode.Verify);
addStepClickLink("00:30:000");
assertOnScreenAt(EditorScreenMode.Verify, 30_000);
addStepClickLink("00:35:000 (0,1)");
assertMovedScreenTo(EditorScreenMode.Compose);
addStepClickLink("00:00:000");
assertOnScreenAt(EditorScreenMode.Compose, 0);
}
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
{
AddStep($"{step} {timestamp}", () =>
Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp))
);
if (waitForSeek)
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
}
private void addStepScreenModeTo(EditorScreenMode screenMode) =>
AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode);
private void assertOnScreenAt(EditorScreenMode screen, double time)
{
AddAssert($"stayed on {screen} at {time}", () =>
editor.Mode.Value == screen
&& editorClock.CurrentTime == time
);
}
private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") =>
AddAssert($"{text} {screen}", () => editor.Mode.Value == screen);
private void setUpEditor(RulesetInfo ruleset)
{
BeatmapSetInfo beatmapSet = null!;
AddStep("Import test beatmap", () =>
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
);
AddStep("Retrieve beatmap", () =>
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
);
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded
);
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor.ReadyForUse);
}
}
}

View File

@ -114,23 +114,25 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestSingleSegment(SplineType splineType, int? degree)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestMultipleSegment(SplineType splineType, int? degree)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
@ -139,9 +141,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.BEZIER, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
});
}
@ -157,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
});
}
@ -170,11 +172,11 @@ namespace osu.Game.Tests.Visual.Gameplay
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
HitWindows = new HitWindows(),
StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) },
},
});

View File

@ -52,59 +52,68 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestSingleSegment(SplineType splineType, int? degree)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(
new PathType { Type = splineType, Degree = degree },
Vector2.Zero,
new Vector2(0, 100),
new Vector2(100),
new Vector2(0, 200),
new Vector2(200)
)));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
[TestCase(SplineType.Linear, null)]
[TestCase(SplineType.BSpline, null)]
[TestCase(SplineType.BSpline, 3)]
[TestCase(SplineType.Catmull, null)]
[TestCase(SplineType.PerfectCurve, null)]
public void TestMultipleSegment(SplineType splineType, int? degree)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
[Test]
public void TestAddControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100))));
AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = new Vector2(100) }));
}
[Test]
public void TestInsertControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100))));
AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = new Vector2(0, 100) }));
}
[Test]
public void TestRemoveControlPoint()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
}
[Test]
public void TestChangePathType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.Bezier);
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.BEZIER);
}
[Test]
public void TestAddSegmentByChangingType()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.Bezier);
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.BEZIER);
}
[Test]
@ -112,8 +121,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.Bezier;
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.BEZIER;
});
AddStep("change second point type to null", () => path.ControlPoints[1].Type = null);
@ -124,8 +133,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.Bezier;
path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints[1].Type = PathType.BEZIER;
});
AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
@ -140,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});
@ -153,35 +162,35 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestLengthenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300);
}
[Test]
public void TestShortenLastSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
}
[Test]
public void TestShortenFirstSegment()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50);
}
[Test]
public void TestShortenToZeroLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0);
}
[Test]
public void TestShortenToNegativeLength()
{
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
}
@ -197,7 +206,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
double[] distances = { 100d, 200d, 300d };
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.LINEAR))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));

View File

@ -437,7 +437,7 @@ namespace osu.Game.Beatmaps.Formats
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PerfectCurve;
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE;
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
@ -453,21 +453,21 @@ namespace osu.Game.Beatmaps.Formats
if (needsExplicitSegment)
{
switch (point.Type)
switch (point.Type?.Type)
{
case PathType.Bezier:
writer.Write("B|");
case SplineType.BSpline:
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
break;
case PathType.Catmull:
case SplineType.Catmull:
writer.Write("C|");
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
writer.Write("P|");
break;
case PathType.Linear:
case SplineType.Linear:
writer.Write("L|");
break;
}

View File

@ -78,14 +78,15 @@ namespace osu.Game.Database
// wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
// which would lead to sliders being mangled when exported back to stable.
// normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
// which outputs a slider path containing only Bezier control points,
// which outputs a slider path containing only BEZIER control points,
// but a non-inherited last control point is (rightly) not considered to be starting a new segment,
// therefore it would fail to clear the `CountSegments() <= 1` check.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to BEZIER.
if (hasPath.Path.ControlPoints.Count > 1)
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);

View File

@ -6,13 +6,15 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
@ -78,6 +80,9 @@ namespace osu.Game.Graphics.UserInterface
{
case StatefulMenuItem stateful:
return new DrawableStatefulMenuItem(stateful);
case OsuMenuItemSpacer spacer:
return new DrawableSpacer(spacer);
}
return new DrawableOsuMenuItem(item);
@ -89,5 +94,28 @@ namespace osu.Game.Graphics.UserInterface
{
Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight
};
protected partial class DrawableSpacer : DrawableOsuMenuItem
{
public DrawableSpacer(MenuItem item)
: base(item)
{
Scale = new Vector2(1, 0.6f);
AddInternal(new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = BackgroundColourHover,
RelativeSizeAxes = Axes.X,
Height = 2f,
Width = 0.8f,
});
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
}
}
}

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Graphics.UserInterface
{
public class OsuMenuItemSpacer : OsuMenuItem
{
public OsuMenuItemSpacer()
: base(" ")
{
}
}
}

View File

@ -119,6 +119,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
/// <summary>
/// "Must be in edit mode to handle editor links"
/// </summary>
public static LocalisableString MustBeInEditorToHandleLinks => new TranslatableString(getKey(@"must_be_in_editor_to_handle_links"), @"Must be in edit mode to handle editor links");
/// <summary>
/// "Failed to parse editor link"
/// </summary>
public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Edit;
namespace osu.Game.Online.Chat
{
@ -41,10 +42,6 @@ namespace osu.Game.Online.Chat
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
RegexOptions.IgnoreCase);
// 00:00:000 (1,2,3) - test
// regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10
private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)");
// #osu
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
@ -274,7 +271,7 @@ namespace osu.Game.Online.Chat
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);

View File

@ -58,6 +58,7 @@ using osu.Game.Performance;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
@ -433,6 +434,9 @@ namespace osu.Game
break;
case LinkAction.OpenEditorTimestamp:
HandleTimestamp(argString);
break;
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
@ -550,6 +554,25 @@ namespace osu.Game
/// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
/// <summary>
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
/// </summary>
public void HandleTimestamp(string timestamp)
{
if (ScreenStack.CurrentScreen is not Editor editor)
{
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.MustBeInEditorToHandleLinks
}));
return;
}
editor.HandleTimestamp(timestamp);
}
/// <summary>
/// Present a skin select immediately.
/// </summary>

View File

@ -151,23 +151,23 @@ namespace osu.Game.Overlays.SkinEditor
{
new MenuItem(CommonStrings.MenuBarFile)
{
Items = new[]
Items = new OsuMenuItem[]
{
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new[]
Items = new OsuMenuItem[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),

View File

@ -14,7 +14,6 @@ using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osu.Game.Utils;
@ -249,7 +248,7 @@ namespace osu.Game.Overlays.SkinEditor
Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray()
};
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () =>
{
@ -277,13 +276,13 @@ namespace osu.Game.Overlays.SkinEditor
}
});
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItemSpacer();
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;

View File

@ -0,0 +1,50 @@
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace osu.Game.Rulesets.Edit
{
public static class EditorTimestampParser
{
// 00:00:000 (...) - test
// original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
public static readonly Regex TIME_REGEX = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled);
public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection)
{
Match match = TIME_REGEX.Match(timestamp);
if (!match.Success)
{
parsedTime = null;
parsedSelection = null;
return false;
}
bool result = true;
result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin);
result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec);
result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec);
// somewhat sane limit for timestamp duration (10 hours).
result &= timeMin < 600;
if (!result)
{
parsedTime = null;
parsedSelection = null;
return false;
}
parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec);
parsedSelection = match.Groups[@"selection"].Value.Trim();
if (!string.IsNullOrEmpty(parsedSelection))
parsedSelection = parsedSelection[1..^1];
return true;
}
}
}

View File

@ -526,8 +526,20 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public abstract bool CursorInPlacementArea { get; }
/// <summary>
/// Returns a string representing the current selection.
/// The inverse method to <see cref="SelectFromTimestamp"/>.
/// </summary>
public virtual string ConvertSelectionToString() => string.Empty;
/// <summary>
/// Selects objects based on the supplied <paramref name="timestamp"/> and <paramref name="objectDescription"/>.
/// The inverse method to <see cref="ConvertSelectionToString"/>.
/// </summary>
/// <param name="timestamp">The time instant to seek to, in milliseconds.</param>
/// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param>
public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { }
#region IPositionSnapProvider
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);

View File

@ -68,32 +68,35 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
var segmentType = controlPoints[start].Type ?? PathType.LINEAR;
switch (segmentType)
switch (segmentType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.Linear:
case SplineType.Linear:
result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position));
break;
default:
case SplineType.BSpline:
if (segmentType.Degree != null)
throw new NotImplementedException("BSpline conversion of arbitrary degree is not implemented.");
foreach (Vector2 v in segmentVertices)
{
result.Add(v + position);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier");
}
// Start the new segment at the current vertex
@ -104,7 +107,7 @@ namespace osu.Game.Rulesets.Objects
}
/// <summary>
/// Converts a path of control points to an identical path using only Bezier type control points.
/// Converts a path of control points to an identical path using only BEZIER type control points.
/// </summary>
/// <param name="controlPoints">The control points of the path.</param>
/// <returns>The list of bezier control points.</returns>
@ -124,49 +127,56 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
var segmentType = controlPoints[start].Type ?? PathType.LINEAR;
switch (segmentType)
switch (segmentType.Type)
{
case PathType.Catmull:
case SplineType.Catmull:
foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null));
}
}
break;
case PathType.Linear:
case SplineType.Linear:
foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null));
}
}
break;
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
var circleResult = ConvertCircleToBezierAnchors(segmentVertices);
for (int j = 0; j < circleResult.Length - 1; j++)
{
result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null));
result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null));
}
break;
case SplineType.BSpline:
var bSplineResult = segmentType.Degree == null
? segmentVertices
: PathApproximator.BSplineToBezier(segmentVertices, segmentType.Degree.Value);
for (int j = 0; j < bSplineResult.Length - 1; j++)
{
result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null));
}
break;
default:
for (int j = 0; j < segmentVertices.Length - 1; j++)
{
result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null));
}
break;
throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier");
}
// Start the new segment at the current vertex

View File

@ -224,16 +224,19 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
default:
case 'C':
return PathType.Catmull;
return PathType.CATMULL;
case 'B':
return PathType.Bezier;
if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0)
return PathType.BSpline(degree);
return PathType.BEZIER;
case 'L':
return PathType.Linear;
return PathType.LINEAR;
case 'P':
return PathType.PerfectCurve;
return PathType.PERFECT_CURVE;
}
}
@ -270,8 +273,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < pointSplit.Length)
{
// Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1).
if (pointSplit[endIndex].Length > 1)
// Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0).
if (!char.IsLetter(pointSplit[endIndex][0]))
continue;
// Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment.
@ -320,14 +323,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
readPoint(endPoint, offset, out vertices[^1]);
// Edge-case rules (to match stable).
if (type == PathType.PerfectCurve)
if (type == PathType.PERFECT_CURVE)
{
if (vertices.Length != 3)
type = PathType.Bezier;
type = PathType.BEZIER;
else if (isLinear(vertices))
{
// osu-stable special-cased colinear perfect curves to a linear path
type = PathType.Linear;
type = PathType.LINEAR;
}
}
@ -349,10 +352,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (vertices[endIndex].Position != vertices[endIndex - 1].Position)
continue;
// Legacy Catmull sliders don't support multiple segments, so adjacent Catmull segments should be treated as a single one.
// Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one.
// Importantly, this is not applied to the first control point, which may duplicate the slider path's position
// resulting in a duplicate (0,0) control point in the resultant list.
if (type == PathType.Catmull && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.

View File

@ -260,7 +260,7 @@ namespace osu.Game.Rulesets.Objects
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = ControlPoints[start].Type ?? PathType.Linear;
var segmentType = ControlPoints[start].Type ?? PathType.LINEAR;
// No need to calculate path when there is only 1 vertex
if (segmentVertices.Length == 1)
@ -288,16 +288,16 @@ namespace osu.Game.Rulesets.Objects
private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, PathType type)
{
switch (type)
switch (type.Type)
{
case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints);
case SplineType.Linear:
return PathApproximator.LinearToPiecewiseLinear(subControlPoints);
case PathType.PerfectCurve:
case SplineType.PerfectCurve:
if (subControlPoints.Length != 3)
break;
List<Vector2> subPath = PathApproximator.ApproximateCircularArc(subControlPoints);
List<Vector2> subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints);
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
if (subPath.Count == 0)
@ -305,11 +305,11 @@ namespace osu.Game.Rulesets.Objects
return subPath;
case PathType.Catmull:
return PathApproximator.ApproximateCatmull(subControlPoints);
case SplineType.Catmull:
return PathApproximator.CatmullToPiecewiseLinear(subControlPoints);
}
return PathApproximator.ApproximateBezier(subControlPoints);
return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length);
}
private void calculateLength()

View File

@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Objects
{
var controlPoints = sliderPath.ControlPoints;
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList();
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
inheritedLinearPoints.ForEach(p => p.Type = PathType.LINEAR);
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects
inheritedLinearPoints.ForEach(p => p.Type = null);
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];

View File

@ -1,13 +1,87 @@
// 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;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Objects.Types
{
public enum PathType
public enum SplineType
{
Catmull,
Bezier,
BSpline,
Linear,
PerfectCurve
}
public readonly struct PathType : IEquatable<PathType>, IHasDescription
{
/// <summary>
/// The type of the spline that should be used to interpret the control points of the path.
/// </summary>
public SplineType Type { get; init; }
/// <summary>
/// The degree of a BSpline. Unused if <see cref="Type"/> is not <see cref="SplineType.BSpline"/>.
/// Null means the degree is equal to the number of control points, 1 means linear, 2 means quadratic, etc.
/// </summary>
public int? Degree { get; init; }
public PathType(SplineType splineType)
{
Type = splineType;
Degree = null;
}
public static readonly PathType CATMULL = new PathType(SplineType.Catmull);
public static readonly PathType BEZIER = new PathType(SplineType.BSpline);
public static readonly PathType LINEAR = new PathType(SplineType.Linear);
public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve);
public static PathType BSpline(int degree)
{
if (degree <= 0)
throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero.");
return new PathType { Type = SplineType.BSpline, Degree = degree };
}
public string Description
{
get
{
switch (Type)
{
case SplineType.Catmull:
return "Catmull";
case SplineType.BSpline:
return Degree == null ? "Bezier" : "B-spline";
case SplineType.Linear:
return "Linear";
case SplineType.PerfectCurve:
return "Perfect curve";
default:
return Type.ToString();
}
}
}
public override int GetHashCode()
=> HashCode.Combine(Type, Degree);
public override bool Equals(object? obj)
=> obj is PathType pathType && Equals(pathType);
public bool Equals(PathType other)
=> Type == other.Type && Degree == other.Degree;
public static bool operator ==(PathType a, PathType b) => a.Equals(b);
public static bool operator !=(PathType a, PathType b) => !a.Equals(b);
public override string ToString() => Description;
}
}

View File

@ -4,11 +4,9 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
@ -151,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
{
switch (item)
{
case EditorMenuItemSpacer spacer:
case OsuMenuItemSpacer spacer:
return new DrawableSpacer(spacer);
case StatefulMenuItem stateful:
@ -195,29 +193,6 @@ namespace osu.Game.Screens.Edit.Components.Menus
Foreground.Padding = new MarginPadding { Vertical = 2 };
}
}
private partial class DrawableSpacer : DrawableOsuMenuItem
{
public DrawableSpacer(MenuItem item)
: base(item)
{
Scale = new Vector2(1, 0.6f);
AddInternal(new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = BackgroundColourHover,
RelativeSizeAxes = Axes.X,
Height = 2f,
Width = 0.8f,
});
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
}
}
}
}

View File

@ -1,13 +0,0 @@
// 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.
namespace osu.Game.Screens.Edit.Components.Menus
{
public class EditorMenuItemSpacer : EditorMenuItem
{
public EditorMenuItemSpacer()
: base(" ")
{
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -50,6 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
Beatmap.SelectedHitObjects.CollectionChanged += updateSelectionLifetime;
if (Composer != null)
{
@ -144,6 +146,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray());
}
/// <summary>
/// Ensures that newly-selected hitobjects are kept alive
/// and drops that keep-alive from newly-deselected objects.
/// </summary>
private void updateSelectionLifetime(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (HitObject newSelection in e.NewItems)
Composer.Playfield.SetKeepAlive(newSelection, true);
}
if (e.OldItems != null)
{
foreach (HitObject oldSelection in e.OldItems)
Composer.Playfield.SetKeepAlive(oldSelection, false);
}
}
protected override void OnBlueprintSelected(SelectionBlueprint<HitObject> blueprint)
{
base.OnBlueprintSelected(blueprint);
@ -166,6 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
Beatmap.HitObjectAdded -= AddBlueprintFor;
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
Beatmap.SelectedHitObjects.CollectionChanged -= updateSelectionLifetime;
}
usageEventBuffer?.Dispose();

View File

@ -14,6 +14,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
@ -39,6 +40,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@ -321,7 +323,7 @@ namespace osu.Game.Screens.Edit
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
@ -1005,12 +1007,12 @@ namespace osu.Game.Screens.Edit
{
createDifficultyCreationMenu(),
createDifficultySwitchMenu(),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } },
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
createExportMenu(),
new EditorMenuItemSpacer(),
new OsuMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit)
};
@ -1130,7 +1132,7 @@ namespace osu.Game.Screens.Edit
foreach (var rulesetBeatmaps in groupedOrderedBeatmaps)
{
if (difficultyItems.Count > 0)
difficultyItems.Add(new EditorMenuItemSpacer());
difficultyItems.Add(new OsuMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps)
{
@ -1150,6 +1152,45 @@ namespace osu.Game.Screens.Edit
loader?.CancelPendingDifficultySwitch();
}
public void HandleTimestamp(string timestamp)
{
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
{
Schedule(() => notifications?.Post(new SimpleErrorNotification
{
Icon = FontAwesome.Solid.ExclamationTriangle,
Text = EditorStrings.FailedToParseEditorLink
}));
return;
}
editorBeatmap.SelectedHitObjects.Clear();
if (clock.IsRunning)
clock.Stop();
double position = timeSpan.Value.TotalMilliseconds;
if (string.IsNullOrEmpty(selection))
{
clock.SeekSmoothlyTo(position);
return;
}
// Seek to the next closest HitObject instead
HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position);
if (nextObject != null)
position = nextObject.StartTime;
clock.SeekSmoothlyTo(position);
Mode.Value = EditorScreenMode.Compose;
// Delegate handling the selection to the ruleset.
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
}
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);

View File

@ -269,13 +269,13 @@ namespace osu.Game.Screens.Play.HUD
barPath = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier),
new PathControlPoint(new Vector2(0, 0), PathType.LINEAR),
new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.BEZIER),
new PathControlPoint(new Vector2(curveStart, 0)),
new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear),
new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier),
new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.LINEAR),
new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.BEZIER),
new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)),
new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear),
new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.LINEAR),
new PathControlPoint(new Vector2(barLength, BarHeight.Value)),
});

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1111.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1121.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1114.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1111.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1121.1" />
</ItemGroup>
</Project>