2021-07-19 04:09:33 +00:00
|
|
|
|
// 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.Catch.Objects;
|
|
|
|
|
using osu.Game.Rulesets.Objects;
|
|
|
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
|
|
|
using osuTK;
|
|
|
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Catch.Tests
|
|
|
|
|
{
|
|
|
|
|
[TestFixture]
|
|
|
|
|
public class JuiceStreamPathTest
|
|
|
|
|
{
|
|
|
|
|
[TestCase(1e3, true, false)]
|
|
|
|
|
// When the coordinates are large, the slope invariant fails within the specified absolute allowance due to the floating-number precision.
|
|
|
|
|
[TestCase(1e9, false, false)]
|
|
|
|
|
// Using discrete values sometimes discover more edge cases.
|
|
|
|
|
[TestCase(10, true, true)]
|
|
|
|
|
public void TestRandomInsertSetPosition(double scale, bool checkSlope, bool integralValues)
|
|
|
|
|
{
|
|
|
|
|
var rng = new Random(1);
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
|
|
|
|
|
for (int iteration = 0; iteration < 100000; iteration++)
|
|
|
|
|
{
|
|
|
|
|
if (rng.Next(10) == 0)
|
|
|
|
|
path.Clear();
|
|
|
|
|
|
|
|
|
|
int vertexCount = path.Vertices.Count;
|
|
|
|
|
|
|
|
|
|
switch (rng.Next(2))
|
|
|
|
|
{
|
|
|
|
|
case 0:
|
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double time = rng.NextDouble() * scale * 2 - scale;
|
2021-07-19 04:09:33 +00:00
|
|
|
|
if (integralValues)
|
2022-05-08 09:32:01 +00:00
|
|
|
|
time = Math.Round(time);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
float oldX = path.PositionAtTime(time);
|
|
|
|
|
int index = path.InsertVertex(time);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
2022-05-08 09:32:01 +00:00
|
|
|
|
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 1:
|
|
|
|
|
{
|
|
|
|
|
int index = rng.Next(path.Vertices.Count);
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double time = path.Vertices[index].Time;
|
2021-07-19 04:09:33 +00:00
|
|
|
|
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
|
|
|
|
if (integralValues)
|
|
|
|
|
newX = MathF.Round(newX);
|
|
|
|
|
|
|
|
|
|
path.SetVertexPosition(index, newX);
|
|
|
|
|
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
2022-05-08 09:32:01 +00:00
|
|
|
|
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
assertInvariants(path.Vertices);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestRemoveVertices()
|
|
|
|
|
{
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
path.Add(10, 5);
|
|
|
|
|
path.Add(20, -5);
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(removeCount, Is.EqualTo(1));
|
|
|
|
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
|
|
|
|
{
|
|
|
|
|
new JuiceStreamPathVertex(0, 0),
|
|
|
|
|
new JuiceStreamPathVertex(20, -5)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
removeCount = path.RemoveVertices((_, i) => i == 0);
|
|
|
|
|
Assert.That(removeCount, Is.EqualTo(1));
|
|
|
|
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
|
|
|
|
{
|
|
|
|
|
new JuiceStreamPathVertex(20, -5)
|
|
|
|
|
}));
|
|
|
|
|
|
2022-06-24 12:25:23 +00:00
|
|
|
|
removeCount = path.RemoveVertices((_, _) => true);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(removeCount, Is.EqualTo(1));
|
|
|
|
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
|
|
|
|
{
|
|
|
|
|
new JuiceStreamPathVertex()
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestResampleVertices()
|
|
|
|
|
{
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
path.Add(-100, -10);
|
|
|
|
|
path.Add(100, 50);
|
|
|
|
|
path.ResampleVertices(new double[]
|
|
|
|
|
{
|
|
|
|
|
-50,
|
|
|
|
|
0,
|
|
|
|
|
70,
|
|
|
|
|
120
|
|
|
|
|
});
|
|
|
|
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
|
|
|
|
{
|
|
|
|
|
new JuiceStreamPathVertex(-100, -10),
|
|
|
|
|
new JuiceStreamPathVertex(-50, -5),
|
|
|
|
|
new JuiceStreamPathVertex(0, 0),
|
|
|
|
|
new JuiceStreamPathVertex(70, 35),
|
|
|
|
|
new JuiceStreamPathVertex(100, 50),
|
|
|
|
|
new JuiceStreamPathVertex(100, 50),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
path.Clear();
|
|
|
|
|
path.SetVertexPosition(0, 10);
|
|
|
|
|
path.ResampleVertices(Array.Empty<double>());
|
|
|
|
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
|
|
|
|
{
|
|
|
|
|
new JuiceStreamPathVertex(0, 10)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
[TestCase(10)]
|
|
|
|
|
[TestCase(0.1)]
|
|
|
|
|
public void TestRandomConvertFromSliderPath(double velocity)
|
2021-07-19 04:09:33 +00:00
|
|
|
|
{
|
|
|
|
|
var rng = new Random(1);
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
var sliderPath = new SliderPath();
|
|
|
|
|
|
|
|
|
|
for (int iteration = 0; iteration < 10000; iteration++)
|
|
|
|
|
{
|
|
|
|
|
sliderPath.ControlPoints.Clear();
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
int start = sliderPath.ControlPoints.Count;
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
float x = (float)(rng.NextDouble() * 1e3);
|
|
|
|
|
float y = (float)(rng.NextDouble() * 1e3);
|
|
|
|
|
sliderPath.ControlPoints.Add(new PathControlPoint(new Vector2(x, y)));
|
|
|
|
|
} while (rng.Next(2) != 0);
|
|
|
|
|
|
|
|
|
|
int length = sliderPath.ControlPoints.Count - start + 1;
|
2023-11-13 07:24:09 +00:00
|
|
|
|
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER;
|
2021-07-19 04:09:33 +00:00
|
|
|
|
} while (rng.Next(3) != 0);
|
|
|
|
|
|
|
|
|
|
if (rng.Next(5) == 0)
|
|
|
|
|
sliderPath.ExpectedDistance.Value = rng.NextDouble() * 3e3;
|
|
|
|
|
else
|
|
|
|
|
sliderPath.ExpectedDistance.Value = null;
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
path.ConvertFromSliderPath(sliderPath, velocity);
|
|
|
|
|
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
|
|
|
|
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
|
|
|
|
assertInvariants(path.Vertices);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double[] sampleTimes = Enumerable.Range(0, 10)
|
|
|
|
|
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
|
|
|
|
.ToArray();
|
2021-07-19 04:09:33 +00:00
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
foreach (double time in sampleTimes)
|
2021-07-19 04:09:33 +00:00
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
|
|
|
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
path.ResampleVertices(sampleTimes);
|
|
|
|
|
assertInvariants(path.Vertices);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
foreach (double time in sampleTimes)
|
2021-07-19 04:09:33 +00:00
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
|
|
|
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestRandomConvertToSliderPath()
|
|
|
|
|
{
|
|
|
|
|
var rng = new Random(1);
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
var sliderPath = new SliderPath();
|
|
|
|
|
|
|
|
|
|
for (int iteration = 0; iteration < 10000; iteration++)
|
|
|
|
|
{
|
|
|
|
|
path.Clear();
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double time = rng.NextDouble() * 1e3;
|
2021-07-19 04:09:33 +00:00
|
|
|
|
float x = (float)(rng.NextDouble() * 1e3);
|
2022-05-08 09:32:01 +00:00
|
|
|
|
path.Add(time, x);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
} while (rng.Next(5) != 0);
|
|
|
|
|
|
|
|
|
|
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
|
|
|
|
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
|
|
|
|
|
|
|
|
|
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
|
|
|
|
|
foreach (var point in sliderPath.ControlPoints)
|
|
|
|
|
{
|
2023-11-08 10:43:54 +00:00
|
|
|
|
Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null);
|
2021-08-25 16:42:57 +00:00
|
|
|
|
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
|
|
|
|
|
|
|
|
|
// The path is preserved only if required velocity is used.
|
|
|
|
|
if (velocity < requiredVelocity) continue;
|
|
|
|
|
|
|
|
|
|
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
|
|
|
|
|
|
2021-07-19 04:09:33 +00:00
|
|
|
|
for (int i = 0; i < 10; i++)
|
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
double time = rng.NextDouble() * path.Duration;
|
|
|
|
|
float expected = path.PositionAtTime(time);
|
|
|
|
|
Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TestInvalidation()
|
|
|
|
|
{
|
|
|
|
|
var path = new JuiceStreamPath();
|
|
|
|
|
Assert.That(path.InvalidationID, Is.EqualTo(1));
|
|
|
|
|
int previousId = path.InvalidationID;
|
|
|
|
|
|
|
|
|
|
path.InsertVertex(10);
|
|
|
|
|
checkNewId();
|
|
|
|
|
|
|
|
|
|
path.SetVertexPosition(1, 5);
|
|
|
|
|
checkNewId();
|
|
|
|
|
|
|
|
|
|
path.Add(20, 0);
|
|
|
|
|
checkNewId();
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
path.RemoveVertices((v, _) => v.Time == 20);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
checkNewId();
|
|
|
|
|
|
|
|
|
|
path.ResampleVertices(new double[] { 5, 10, 15 });
|
|
|
|
|
checkNewId();
|
|
|
|
|
|
|
|
|
|
path.Clear();
|
|
|
|
|
checkNewId();
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
path.ConvertFromSliderPath(new SliderPath(), 1);
|
2021-07-19 04:09:33 +00:00
|
|
|
|
checkNewId();
|
|
|
|
|
|
|
|
|
|
void checkNewId()
|
|
|
|
|
{
|
|
|
|
|
Assert.That(path.InvalidationID, Is.Not.EqualTo(previousId));
|
|
|
|
|
previousId = path.InvalidationID;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-08 09:32:01 +00:00
|
|
|
|
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
2021-07-19 04:09:33 +00:00
|
|
|
|
{
|
|
|
|
|
Assert.That(vertices, Is.Not.Empty);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < vertices.Count; i++)
|
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
Assert.That(double.IsFinite(vertices[i].Time));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
Assert.That(float.IsFinite(vertices[i].X));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i < vertices.Count; i++)
|
|
|
|
|
{
|
2022-05-08 09:32:01 +00:00
|
|
|
|
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
2021-07-19 04:09:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|