mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 19:42:55 +08:00
Merge branch 'master' into delete-all-beatmap-videos-sbs
This commit is contained in:
commit
cb383d4bdc
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/inspectcode
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig') }}
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
|
||||
|
||||
- name: Dotnet code style
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
||||
@ -78,15 +78,6 @@ jobs:
|
||||
with:
|
||||
dotnet-version: "6.0.x"
|
||||
|
||||
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
|
||||
# https://github.com/ppy/osu-framework/issues/4349
|
||||
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
|
||||
- name: Install libavformat-dev
|
||||
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
|
||||
run: |
|
||||
sudo apt-get update && \
|
||||
sudo apt-get -y install libavformat-dev
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
|
2
.github/workflows/sentry-release.yml
vendored
2
.github/workflows/sentry-release.yml
vendored
@ -23,4 +23,4 @@ jobs:
|
||||
SENTRY_URL: https://sentry.ppy.sh/
|
||||
with:
|
||||
environment: production
|
||||
version: ${{ github.ref }}
|
||||
version: osu@${{ github.ref_name }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -340,3 +340,5 @@ inspectcode
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
**/FodyWeavers.xml
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SwUserDefinedSpecifications">
|
||||
<option name="specTypeByUrl">
|
||||
<map />
|
||||
</option>
|
||||
</component>
|
||||
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
|
||||
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
|
||||
</component>
|
||||
</project>
|
@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
||||
|
@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.511.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.605.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
<PackageReference Include="Realm" Version="10.11.2" />
|
||||
<PackageReference Include="Realm" Version="10.14.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -18,5 +18,6 @@
|
||||
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
</files>
|
||||
</package>
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||
{
|
||||
private const double velocity = 0.5;
|
||||
private const double velocity_factor = 0.5;
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVelocityLimit()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300 });
|
||||
}
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
[Test]
|
||||
public void TestPreviousVerticesAreFixed()
|
||||
{
|
||||
double[] times = { 100, 300, 500, 700 };
|
||||
float[] positions = { 200, 400, 100, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
|
||||
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
{
|
||||
double[] times = { 100, 300, 500 };
|
||||
float[] positions = { 200, 200, 0, 250 };
|
||||
float[] positions = { 200, 200, -3000, 250 };
|
||||
|
||||
addMoveAndClickSteps(times[0], positions[0]);
|
||||
addMoveAndClickSteps(times[1], positions[1]);
|
||||
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFirstVertexIsFixed()
|
||||
{
|
||||
double[] times = { 100, 200 };
|
||||
float[] positions = { 100, 300 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 100, 150 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrder()
|
||||
{
|
||||
|
@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
const double velocity = 0.25;
|
||||
double[] times = { 100, 500, 700 };
|
||||
float[] positions = { 100, 100, 100 };
|
||||
addBlueprintStep(times, positions, velocity);
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
|
||||
AddMouseMoveStep(times[1], 200);
|
||||
addVertexCheckStep(3, 1, times[1], 200);
|
||||
addVertexCheckStep(3, 2, times[2], 150);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
addVertexCheckStep(3, 1, times[1], 100);
|
||||
// Stored position is restored.
|
||||
addVertexCheckStep(3, 2, times[2], positions[2]);
|
||||
|
||||
AddMouseMoveStep(times[1], 300);
|
||||
addDragEndStep();
|
||||
addDragStartStep(times[1], 300);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
// Position is different because a changed position is committed when the previous drag is ended.
|
||||
addVertexCheckStep(3, 2, times[2], 250);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addAddVertexSteps(500, 150);
|
||||
addVertexCheckStep(3, 1, 500, 150);
|
||||
|
||||
addAddVertexSteps(90, 220);
|
||||
addAddVertexSteps(90, 200);
|
||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||
|
||||
addAddVertexSteps(750, 180);
|
||||
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var path = new JuiceStreamPath();
|
||||
for (int i = 1; i < times.Length; i++)
|
||||
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
||||
path.Add(times[i] - times[0], positions[i] - positions[0]);
|
||||
|
||||
var sliderPath = new SliderPath();
|
||||
path.ConvertToSliderPath(sliderPath, 0);
|
||||
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||
}
|
||||
|
||||
@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
||||
{
|
||||
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
|
||||
double expectedTime = time - hitObject.StartTime;
|
||||
float expectedX = x - hitObject.OriginalX;
|
||||
var vertices = getVertices();
|
||||
return vertices.Count == count &&
|
||||
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
||||
double time = rng.NextDouble() * scale * 2 - scale;
|
||||
if (integralValues)
|
||||
distance = Math.Round(distance);
|
||||
time = Math.Round(time);
|
||||
|
||||
float oldX = path.PositionAtDistance(distance);
|
||||
int index = path.InsertVertex(distance);
|
||||
float oldX = path.PositionAtTime(time);
|
||||
int index = path.InsertVertex(time);
|
||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||
break;
|
||||
}
|
||||
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
case 1:
|
||||
{
|
||||
int index = rng.Next(path.Vertices.Count);
|
||||
double distance = path.Vertices[index].Distance;
|
||||
double time = path.Vertices[index].Time;
|
||||
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));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertInvariants(path.Vertices, checkSlope);
|
||||
assertInvariants(path.Vertices);
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(10, 5);
|
||||
path.Add(20, -5);
|
||||
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
|
||||
Assert.That(removeCount, Is.EqualTo(1));
|
||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||
{
|
||||
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomConvertFromSliderPath()
|
||||
[TestCase(10)]
|
||||
[TestCase(0.1)]
|
||||
public void TestRandomConvertFromSliderPath(double velocity)
|
||||
{
|
||||
var rng = new Random(1);
|
||||
var path = new JuiceStreamPath();
|
||||
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
else
|
||||
sliderPath.ExpectedDistance.Value = null;
|
||||
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||
assertInvariants(path.Vertices, true);
|
||||
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);
|
||||
|
||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
||||
.ToArray();
|
||||
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||
.ToArray();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
|
||||
path.ResampleVertices(sampleDistances);
|
||||
assertInvariants(path.Vertices, true);
|
||||
path.ResampleVertices(sampleTimes);
|
||||
assertInvariants(path.Vertices);
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
do
|
||||
{
|
||||
double distance = rng.NextDouble() * 1e3;
|
||||
double time = rng.NextDouble() * 1e3;
|
||||
float x = (float)(rng.NextDouble() * 1e3);
|
||||
path.Add(distance, x);
|
||||
path.Add(time, x);
|
||||
} while (rng.Next(5) != 0);
|
||||
|
||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
assertInvariants(path.Vertices, true);
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||
|
||||
foreach (var point in sliderPath.ControlPoints)
|
||||
{
|
||||
@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double distance = rng.NextDouble() * path.Distance;
|
||||
float expected = path.PositionAtDistance(distance);
|
||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(20, 0);
|
||||
checkNewId();
|
||||
|
||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
||||
path.RemoveVertices((v, _) => v.Time == 20);
|
||||
checkNewId();
|
||||
|
||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Clear();
|
||||
checkNewId();
|
||||
|
||||
path.ConvertFromSliderPath(new SliderPath());
|
||||
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||
checkNewId();
|
||||
|
||||
void checkNewId()
|
||||
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
||||
{
|
||||
Assert.That(vertices, Is.Not.Empty);
|
||||
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(double.IsFinite(vertices[i].Distance));
|
||||
Assert.That(double.IsFinite(vertices[i].Time));
|
||||
Assert.That(float.IsFinite(vertices[i].X));
|
||||
}
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
||||
|
||||
if (!checkSlope) continue;
|
||||
|
||||
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
|
||||
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
|
||||
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
|
||||
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public int VertexCount => path.Vertices.Count;
|
||||
|
||||
protected readonly Func<float, double> PositionToDistance;
|
||||
protected readonly Func<float, double> PositionToTime;
|
||||
|
||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||
|
||||
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
|
||||
protected EditablePath(Func<float, double> positionToDistance)
|
||||
protected EditablePath(Func<float, double> positionToTime)
|
||||
{
|
||||
PositionToDistance = positionToDistance;
|
||||
PositionToTime = positionToTime;
|
||||
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
while (InternalChildren.Count < path.Vertices.Count)
|
||||
AddInternal(new VertexPiece());
|
||||
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
var piece = (VertexPiece)InternalChildren[i];
|
||||
var vertex = path.Vertices[i];
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
|
||||
piece.UpdateFrom(vertexStates[i]);
|
||||
}
|
||||
}
|
||||
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||
{
|
||||
var sliderPath = hitObject.Path;
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
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))
|
||||
{
|
||||
path.ResampleVertices(hitObject.NestedHitObjects
|
||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
|
||||
.Select(h => h.StartTime - hitObject.StartTime));
|
||||
}
|
||||
|
||||
vertexStates.Clear();
|
||||
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
|
||||
// but it should be always rounded up to satisfy the required minimum velocity condition.
|
||||
//
|
||||
// This is rounded to integers instead of using the precision of the bindable
|
||||
// because it results in a smaller number of non-redundant control points.
|
||||
//
|
||||
// The value is clamped here by the bindable min and max values.
|
||||
// In case the required velocity is too large, the path is not preserved.
|
||||
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||
|
||||
if (beatSnapProvider == null) return;
|
||||
|
||||
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
||||
double endTime = hitObject.StartTime + path.Duration;
|
||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||
}
|
||||
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
protected int AddVertex(double distance, float x)
|
||||
protected int AddVertex(double time, float x)
|
||||
{
|
||||
int index = path.InsertVertex(distance);
|
||||
int index = path.InsertVertex(time);
|
||||
path.SetVertexPosition(index, x);
|
||||
vertexStates.Insert(index, new VertexState());
|
||||
|
||||
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
||||
protected void MoveSelectedVertices(double timeDelta, float xDelta)
|
||||
{
|
||||
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
||||
// Because the vertex list may be reordered due to time change, the state list must be reordered as well.
|
||||
previousVertexStates.Clear();
|
||||
previousVertexStates.AddRange(vertexStates);
|
||||
|
||||
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
for (int i = 1; i < vertexCount; i++)
|
||||
{
|
||||
var state = previousVertexStates[i];
|
||||
double distance = state.VertexBeforeChange.Distance;
|
||||
double time = state.VertexBeforeChange.Time;
|
||||
if (state.IsSelected)
|
||||
distance += distanceDelta;
|
||||
time += timeDelta;
|
||||
|
||||
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
||||
int newIndex = path.InsertVertex(Math.Max(0, time));
|
||||
vertexStates.Insert(newIndex, state);
|
||||
}
|
||||
|
||||
|
@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
/// </summary>
|
||||
private JuiceStreamPathVertex lastVertex;
|
||||
|
||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddNewVertex()
|
||||
{
|
||||
var endVertex = Vertices[^1];
|
||||
int index = AddVertex(endVertex.Distance, endVertex.X);
|
||||
int index = AddVertex(endVertex.Time, endVertex.X);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||
{
|
||||
Vector2 position = ToRelativePosition(screenSpacePosition);
|
||||
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
|
||||
double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
|
||||
float xDelta = position.X - lastVertex.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
private readonly Path drawablePath;
|
||||
|
||||
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
|
||||
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
||||
|
||||
public ScrollingPath()
|
||||
{
|
||||
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
computeDistanceXs(hitObject);
|
||||
computeTimeXs(hitObject);
|
||||
drawablePath.Vertices = vertices
|
||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
||||
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||
.ToArray();
|
||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
private void computeDistanceXs(JuiceStream hitObject)
|
||||
private void computeTimeXs(JuiceStream hitObject)
|
||||
{
|
||||
vertices.Clear();
|
||||
|
||||
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
if (sliderVertices.Count == 0)
|
||||
return;
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
Vector2 lastPosition = Vector2.Zero;
|
||||
|
||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||
{
|
||||
foreach (var position in sliderVertices)
|
||||
{
|
||||
distance += Vector2.Distance(lastPosition, position);
|
||||
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||
lastPosition = position;
|
||||
|
||||
vertices.Add((distance, position.X));
|
||||
vertices.Add((time, position.X));
|
||||
}
|
||||
|
||||
sliderVertices.Reverse();
|
||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddVertex(Vector2 relativePosition)
|
||||
{
|
||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
||||
int index = AddVertex(distance, relativePosition.X);
|
||||
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||
int index = AddVertex(time, relativePosition.X);
|
||||
selectOnly(index);
|
||||
}
|
||||
|
||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
||||
double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
|
||||
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new PlacementEditablePath(positionToDistance)
|
||||
editablePath = new PlacementEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new SelectionEditablePath(positionToDistance)
|
||||
editablePath = new SelectionEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
|
||||
private void initializeJuiceStreamPath()
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
result = snapResult;
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity { get; private set; }
|
||||
private double velocityFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance { get; private set; }
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
||||
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
||||
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
|
||||
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
|
||||
/// and this slope condition is always maintained as an invariant.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class JuiceStreamPath
|
||||
{
|
||||
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int InvalidationID { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
|
||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
|
||||
/// </summary>
|
||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
||||
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||
|
||||
/// <remarks>
|
||||
/// This list should always be non-empty.
|
||||
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Compute the x-position of the path at the given <paramref name="distance"/>.
|
||||
/// Compute the x-position of the path at the given <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
|
||||
/// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
|
||||
/// </remarks>
|
||||
public float PositionAtDistance(double distance)
|
||||
public float PositionAtTime(double time)
|
||||
{
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
return positionAtDistance(distance, index);
|
||||
int index = vertexIndexAtTime(time);
|
||||
return positionAtTime(time, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert a vertex at given <paramref name="distance"/>.
|
||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
||||
/// Insert a vertex at given <paramref name="time"/>.
|
||||
/// The <see cref="PositionAtTime"/> is used as the position of the new vertex.
|
||||
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
||||
/// </summary>
|
||||
/// <returns>The index of the new vertex.</returns>
|
||||
public int InsertVertex(double distance)
|
||||
public int InsertVertex(double time)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(time));
|
||||
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
float x = positionAtDistance(distance, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
||||
int index = vertexIndexAtTime(time);
|
||||
float x = positionAtTime(time, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||
|
||||
invalidate();
|
||||
return index;
|
||||
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
/// <summary>
|
||||
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
||||
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
|
||||
/// </summary>
|
||||
public void SetVertexPosition(int index, float newX)
|
||||
{
|
||||
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
if (!float.IsFinite(newX))
|
||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||
|
||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
||||
|
||||
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
for (int i = index + 1; i < vertices.Count; i++)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
vertices[index] = newVertex;
|
||||
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
||||
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||
/// </summary>
|
||||
public void Add(double distance, float x)
|
||||
public void Add(double time, float x)
|
||||
{
|
||||
int index = InsertVertex(distance);
|
||||
int index = InsertVertex(time);
|
||||
SetVertexPosition(index, x);
|
||||
}
|
||||
|
||||
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate this path by using difference set of vertices at given distances.
|
||||
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
|
||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
|
||||
/// Recreate this path by using difference set of vertices at given time points.
|
||||
/// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
|
||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||
/// </summary>
|
||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
||||
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||
{
|
||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||
|
||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
||||
float x = PositionAtDistance(clampedDistance);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
||||
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||
float x = PositionAtTime(clampedTime);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||
}
|
||||
|
||||
sampledVertices.Sort();
|
||||
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// <remarks>
|
||||
/// Duplicated vertices are automatically removed.
|
||||
/// </remarks>
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||
{
|
||||
var sliderPathVertices = new List<Vector2>();
|
||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
|
||||
vertices.Clear();
|
||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||
|
||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
||||
{
|
||||
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
|
||||
time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
|
||||
|
||||
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
|
||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
||||
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||
Add(time, sliderPathVertices[i].X);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
|
||||
/// </summary>
|
||||
public double ComputeRequiredVelocity()
|
||||
{
|
||||
double maximumSlope = 0;
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
|
||||
double timeDifference = vertices[i].Time - vertices[i - 1].Time;
|
||||
|
||||
// A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
|
||||
if (Precision.AlmostEquals(timeDifference, 0))
|
||||
continue;
|
||||
|
||||
maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
|
||||
}
|
||||
|
||||
return maximumSlope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
||||
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
||||
///
|
||||
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
|
||||
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
|
||||
/// </summary>
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||
{
|
||||
const float margin = 1;
|
||||
|
||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
||||
double currentDistance = 0;
|
||||
double currentTime = 0;
|
||||
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||
|
||||
sliderPath.ControlPoints.Clear();
|
||||
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||
|
||||
float deltaX = vertices[i].X - lastPosition.X;
|
||||
double length = vertices[i].Distance - currentDistance;
|
||||
double length = (vertices[i].Time - currentTime) * velocity;
|
||||
|
||||
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
||||
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
|
||||
// The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
|
||||
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||
|
||||
// When `deltaY` is small, one segment is always enough.
|
||||
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
||||
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||
lastPosition = nextPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
|
||||
/// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
|
||||
/// </summary>
|
||||
private int vertexIndexAtDistance(double distance)
|
||||
private int vertexIndexAtTime(double time)
|
||||
{
|
||||
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
||||
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||
return i < 0 ? ~i : i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
|
||||
/// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
|
||||
/// </summary>
|
||||
private float positionAtDistance(double distance, int index)
|
||||
private float positionAtTime(double time, int index)
|
||||
{
|
||||
if (index <= 0)
|
||||
return vertices[0].X;
|
||||
if (index >= vertices.Count)
|
||||
return vertices[^1].X;
|
||||
|
||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
||||
if (Precision.AlmostEquals(length, 0))
|
||||
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||
if (Precision.AlmostEquals(duration, 0))
|
||||
return vertices[index].X;
|
||||
|
||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||
|
||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the two vertices can connected directly while satisfying the slope condition.
|
||||
/// </summary>
|
||||
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
|
||||
{
|
||||
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
|
||||
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
|
||||
return xDistance <= length + allowance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
|
||||
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
|
||||
/// </summary>
|
||||
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
|
||||
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
|
||||
{
|
||||
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
|
||||
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
|
||||
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||
}
|
||||
|
||||
private void invalidate() => InvalidationID++;
|
||||
|
@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||
{
|
||||
public readonly double Distance;
|
||||
public readonly double Time;
|
||||
|
||||
public readonly float X;
|
||||
|
||||
public JuiceStreamPathVertex(double distance, float x)
|
||||
public JuiceStreamPathVertex(double time, float x)
|
||||
{
|
||||
Distance = distance;
|
||||
Time = time;
|
||||
X = x;
|
||||
}
|
||||
|
||||
public int CompareTo(JuiceStreamPathVertex other)
|
||||
{
|
||||
int c = Distance.CompareTo(other.Distance);
|
||||
int c = Time.CompareTo(other.Time);
|
||||
return c != 0 ? c : X.CompareTo(other.X);
|
||||
}
|
||||
|
||||
public override string ToString() => $"({Distance}, {X})";
|
||||
public override string ToString() => $"({Time}, {X})";
|
||||
}
|
||||
}
|
||||
|
@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -45,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap),
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
|
@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
|
||||
|
||||
[TestCase(2.3449735700206298d, 151, "diffcalc-test")]
|
||||
[TestCase(2.3449735700206298d, 242, "diffcalc-test")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(2.7879104989252959d, 151, "diffcalc-test")]
|
||||
[TestCase(2.7879104989252959d, 242, "diffcalc-test")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
||||
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
// Todo: osu!mania doesn't output MaxCombo attribute for some reason.
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
||||
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
|
||||
|
@ -52,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
||||
};
|
||||
}
|
||||
|
||||
private static int maxComboForObject(HitObject hitObject)
|
||||
{
|
||||
if (hitObject is HoldNote hold)
|
||||
return 1 + (int)((hold.EndTime - hold.StartTime) / 100);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||
|
@ -5,7 +5,10 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = result.Playfield as Column;
|
||||
if (result.Playfield is Column col)
|
||||
{
|
||||
// Apply an offset to better align with the visual grid.
|
||||
// This should only be applied during placement, as during selection / drag operations the movement is relative
|
||||
// to the initial point of interaction rather than the grid.
|
||||
switch (col.ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = col;
|
||||
}
|
||||
}
|
||||
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
|
||||
switch (ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getNoteHeight() =>
|
||||
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
|
||||
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
{
|
||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
MuteComboCount = { Value = 0 },
|
||||
},
|
||||
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
|
||||
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
|
||||
Player.ChildrenOfType<MetronomeBeat>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
|
@ -17,11 +17,11 @@ using osu.Framework.Testing.Input;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public TestSceneGameplayCursor()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
|
||||
gameplayState = TestGameplayState.Create(ruleset);
|
||||
|
||||
AddStep("change background colour", () =>
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return snapResult;
|
||||
|
||||
return new SnapResult(screenSpacePosition, null);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
||||
return positionSnap;
|
||||
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
if (positionInfo == positionInfos.First())
|
||||
{
|
||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
|
||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||
}
|
||||
else
|
||||
|
@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
[Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
|
||||
private DrawableHitObject? drawableObject { get; set; }
|
||||
|
||||
[Resolved]
|
||||
|
@ -78,7 +78,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
}
|
||||
});
|
||||
|
||||
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
|
||||
var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
|
||||
|
||||
if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
|
||||
{
|
||||
AddInternal(ApproachCircle = new Sprite
|
||||
{
|
||||
|
@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||
|
||||
@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
if (!(osuObject is Slider slider))
|
||||
return;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
|
||||
@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a slider about its start position by the specified angle.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to be rotated.</param>
|
||||
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||
public static void RotateSlider(Slider slider, float rotation)
|
||||
{
|
||||
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
||||
|
||||
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(rotateNestedObject);
|
||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(rotateNestedObject);
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||
foreach (var point in controlPoints)
|
||||
point.Position = rotateVector(point.Position, rotation);
|
||||
|
||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a vector by the specified angle.
|
||||
/// </summary>
|
||||
/// <param name="vector">The vector to be rotated.</param>
|
||||
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
|
||||
/// <returns>The rotated vector.</returns>
|
||||
private static Vector2 rotateVector(Vector2 vector, float rotation)
|
||||
{
|
||||
float angle = MathF.Atan2(vector.Y, vector.X) + rotation;
|
||||
float length = vector.Length;
|
||||
return new Vector2(
|
||||
length * MathF.Cos(angle),
|
||||
length * MathF.Sin(angle)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
foreach (OsuHitObject hitObject in hitObjects)
|
||||
{
|
||||
Vector2 relativePosition = hitObject.Position - previousPosition;
|
||||
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
||||
float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||
float relativeAngle = absoluteAngle - previousAngle;
|
||||
|
||||
positionInfos.Add(new ObjectPositionInfo(hitObject)
|
||||
ObjectPositionInfo positionInfo;
|
||||
positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
|
||||
{
|
||||
RelativeAngle = relativeAngle,
|
||||
DistanceFromPrevious = relativePosition.Length
|
||||
});
|
||||
|
||||
if (hitObject is Slider slider)
|
||||
{
|
||||
float absoluteRotation = getSliderRotation(slider);
|
||||
positionInfo.Rotation = absoluteRotation - absoluteAngle;
|
||||
absoluteAngle = absoluteRotation;
|
||||
}
|
||||
|
||||
previousPosition = hitObject.EndPosition;
|
||||
previousAngle = absoluteAngle;
|
||||
}
|
||||
@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
if (hitObject is Spinner)
|
||||
{
|
||||
previous = null;
|
||||
previous = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
||||
if (previous.HitObject is Slider s)
|
||||
{
|
||||
previousAbsoluteAngle = getSliderRotation(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||
previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||
}
|
||||
}
|
||||
|
||||
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
|
||||
|
||||
var posRelativeToPrev = new Vector2(
|
||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
|
||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
|
||||
current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
|
||||
current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
|
||||
);
|
||||
|
||||
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
||||
@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
||||
|
||||
current.PositionModified = lastEndPosition + posRelativeToPrev;
|
||||
|
||||
if (!(current.HitObject is Slider slider))
|
||||
return;
|
||||
|
||||
absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
||||
|
||||
Vector2 centreOfMassOriginal = calculateCentreOfMass(slider);
|
||||
Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider));
|
||||
centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified);
|
||||
|
||||
float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X);
|
||||
if (!Precision.AlmostEquals(relativeRotation, 0))
|
||||
RotateSlider(slider, relativeRotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
var previousPosition = workingObject.PositionModified;
|
||||
|
||||
// Clamp slider position to the placement area
|
||||
// If the slider is larger than the playfield, force it to stay at the original position
|
||||
// If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
|
||||
float newX = possibleMovementBounds.Width < 0
|
||||
? workingObject.PositionOriginal.X
|
||||
? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X)
|
||||
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
||||
|
||||
float newY = possibleMovementBounds.Height < 0
|
||||
? workingObject.PositionOriginal.Y
|
||||
? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y)
|
||||
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
||||
|
||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||
@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the centre of mass of a slider relative to its start position.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to process.</param>
|
||||
/// <returns>The centre of mass of the slider.</returns>
|
||||
private static Vector2 calculateCentreOfMass(Slider slider)
|
||||
{
|
||||
const double sample_step = 50;
|
||||
|
||||
// just sample the start and end positions if the slider is too short
|
||||
if (slider.Distance <= sample_step)
|
||||
{
|
||||
return Vector2.Divide(slider.Path.PositionAt(1), 2);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
Vector2 sum = Vector2.Zero;
|
||||
double pathDistance = slider.Distance;
|
||||
|
||||
for (double i = 0; i < pathDistance; i += sample_step)
|
||||
{
|
||||
sum += slider.Path.PositionAt(i / pathDistance);
|
||||
count++;
|
||||
}
|
||||
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider to process.</param>
|
||||
/// <returns>The angle in radians.</returns>
|
||||
private static float getSliderRotation(Slider slider)
|
||||
{
|
||||
var endPositionVector = slider.Path.PositionAt(1);
|
||||
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
|
||||
}
|
||||
|
||||
public class ObjectPositionInfo
|
||||
{
|
||||
/// <summary>
|
||||
@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
/// </remarks>
|
||||
public float DistanceFromPrevious { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The rotation of the hit object, relative to its jump angle.
|
||||
/// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle.
|
||||
/// For hit circles and spinners, this property is ignored.
|
||||
/// </summary>
|
||||
public float Rotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
||||
/// </summary>
|
||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
|
||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(1.9971301024093662d, 200, "diffcalc-test")]
|
||||
[TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
|
||||
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(3.1645810961313674d, 200, "diffcalc-test")]
|
||||
[TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||
|
||||
|
@ -1,145 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects special hit object patterns which are easier to hit using special techniques
|
||||
/// than normally assumed in the fully-alternating play style.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This component detects two basic types of patterns, leveraged by the following techniques:
|
||||
/// <list>
|
||||
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
|
||||
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public class StaminaCheeseDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
|
||||
/// </summary>
|
||||
private const int roll_min_repetitions = 12;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
|
||||
/// </summary>
|
||||
private const int tl_min_repetitions = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
|
||||
/// </summary>
|
||||
private readonly List<TaikoDifficultyHitObject> hitObjects;
|
||||
|
||||
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
this.hitObjects = hitObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
|
||||
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
|
||||
/// </summary>
|
||||
public void FindCheese()
|
||||
{
|
||||
findRolls(3);
|
||||
findRolls(4);
|
||||
|
||||
findTlTap(0, HitType.Rim);
|
||||
findTlTap(1, HitType.Rim);
|
||||
findTlTap(0, HitType.Centre);
|
||||
findTlTap(1, HitType.Centre);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all sequences hittable using a roll.
|
||||
/// </summary>
|
||||
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
|
||||
private void findRolls(int patternLength)
|
||||
{
|
||||
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
|
||||
|
||||
// for convenience, we're tracking the index of the item *before* our suspected repeat's start,
|
||||
// as that index can be simply subtracted from the current index to get the number of elements in between
|
||||
// without off-by-one errors
|
||||
int indexBeforeLastRepeat = -1;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; i++)
|
||||
{
|
||||
history.Enqueue(hitObjects[i]);
|
||||
if (!history.Full)
|
||||
continue;
|
||||
|
||||
if (!containsPatternRepeat(history, patternLength))
|
||||
{
|
||||
// we're setting this up for the next iteration, hence the +1.
|
||||
// right here this index will point at the queue's front (oldest item),
|
||||
// but that item is about to be popped next loop with an enqueue.
|
||||
indexBeforeLastRepeat = i - history.Count + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int repeatedLength = i - indexBeforeLastRepeat;
|
||||
if (repeatedLength < roll_min_repetitions)
|
||||
continue;
|
||||
|
||||
markObjectsAsCheese(Math.Max(lastMarkEnd, i - repeatedLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
|
||||
/// </summary>
|
||||
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
|
||||
{
|
||||
for (int j = 0; j < patternLength; j++)
|
||||
{
|
||||
if (history[j].HitType != history[j + patternLength].HitType)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all sequences hittable using a TL tap.
|
||||
/// </summary>
|
||||
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
|
||||
/// <param name="type">The type of hit to check for TL taps.</param>
|
||||
private void findTlTap(int parity, HitType type)
|
||||
{
|
||||
int tlLength = -2;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
for (int i = parity; i < hitObjects.Count; i += 2)
|
||||
{
|
||||
if (hitObjects[i].HitType == type)
|
||||
tlLength += 2;
|
||||
else
|
||||
tlLength = -2;
|
||||
|
||||
if (tlLength < tl_min_repetitions)
|
||||
continue;
|
||||
|
||||
markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all objects from <paramref name="start"/> to <paramref name="end"/> (inclusive) as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||
/// </summary>
|
||||
private void markObjectsAsCheese(int start, int end)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
hitObjects[i].StaminaCheese = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Stamina of a single key, calculated based on repetition speed.
|
||||
/// </summary>
|
||||
public class SingleKeyStamina
|
||||
{
|
||||
private double? previousHitTime;
|
||||
|
||||
/// <summary>
|
||||
/// Similar to <see cref="StrainDecaySkill.StrainValueOf"/>
|
||||
/// </summary>
|
||||
public double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (previousHitTime == null)
|
||||
{
|
||||
previousHitTime = current.StartTime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double objectStrain = 0.5;
|
||||
objectStrain += speedBonus(current.StartTime - previousHitTime.Value);
|
||||
previousHitTime = current.StartTime;
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the same key.</param>
|
||||
private double speedBonus(double notePairDuration)
|
||||
{
|
||||
return 175 / (notePairDuration + 100);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -22,39 +20,52 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
|
||||
/// </summary>
|
||||
private const int max_history_length = 2;
|
||||
private readonly SingleKeyStamina[] centreKeyStamina =
|
||||
{
|
||||
new SingleKeyStamina(),
|
||||
new SingleKeyStamina()
|
||||
};
|
||||
|
||||
private readonly SingleKeyStamina[] rimKeyStamina =
|
||||
{
|
||||
new SingleKeyStamina(),
|
||||
new SingleKeyStamina()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
|
||||
/// Current index into <see cref="centreKeyStamina" /> for a centre hit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
|
||||
/// This naturally translates onto index offsets of the objects in the map.
|
||||
/// </remarks>
|
||||
private readonly int hand;
|
||||
private int centreKeyIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
|
||||
/// Current index into <see cref="rimKeyStamina" /> for a rim hit.
|
||||
/// </summary>
|
||||
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
|
||||
|
||||
/// <summary>
|
||||
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
|
||||
/// </summary>
|
||||
private double offhandObjectDuration = double.MaxValue;
|
||||
private int rimKeyIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Stamina"/> skill.
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
|
||||
public Stamina(Mod[] mods, bool rightHand)
|
||||
public Stamina(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
hand = rightHand ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next <see cref="SingleKeyStamina"/> to use for the given <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/>.</param>
|
||||
private SingleKeyStamina getNextSingleKeyStamina(TaikoDifficultyHitObject current)
|
||||
{
|
||||
// Alternate key for the same color.
|
||||
if (current.HitType == HitType.Centre)
|
||||
{
|
||||
centreKeyIndex = (centreKeyIndex + 1) % 2;
|
||||
return centreKeyStamina[centreKeyIndex];
|
||||
}
|
||||
|
||||
rimKeyIndex = (rimKeyIndex + 1) % 2;
|
||||
return rimKeyStamina[rimKeyIndex];
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
@ -65,52 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
}
|
||||
|
||||
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||
|
||||
if (hitObject.ObjectIndex % 2 == hand)
|
||||
{
|
||||
double objectStrain = 1;
|
||||
|
||||
if (hitObject.ObjectIndex == 1)
|
||||
return 1;
|
||||
|
||||
notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
|
||||
|
||||
double shortestRecentNote = notePairDurationHistory.Min();
|
||||
objectStrain += speedBonus(shortestRecentNote);
|
||||
|
||||
if (hitObject.StaminaCheese)
|
||||
objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
|
||||
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
offhandObjectDuration = hitObject.DeltaTime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||
private double cheesePenalty(double notePairDuration)
|
||||
{
|
||||
if (notePairDuration > 125) return 1;
|
||||
if (notePairDuration < 100) return 0.6;
|
||||
|
||||
return 0.6 + (notePairDuration - 100) * 0.016;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||
private double speedBonus(double notePairDuration)
|
||||
{
|
||||
if (notePairDuration >= 200) return 0;
|
||||
|
||||
double bonus = 200 - notePairDuration;
|
||||
bonus *= bonus;
|
||||
return bonus / 100000;
|
||||
return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
private const double rhythm_skill_multiplier = 0.014;
|
||||
private const double colour_skill_multiplier = 0.01;
|
||||
private const double stamina_skill_multiplier = 0.02;
|
||||
private const double stamina_skill_multiplier = 0.021;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@ -33,8 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
new Colour(mods),
|
||||
new Rhythm(mods),
|
||||
new Stamina(mods, true),
|
||||
new Stamina(mods, false),
|
||||
new Stamina(mods)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
@ -58,7 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
);
|
||||
}
|
||||
|
||||
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
|
||||
return taikoDifficultyHitObjects;
|
||||
}
|
||||
|
||||
@ -69,17 +67,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
var colour = (Colour)skills[0];
|
||||
var rhythm = (Rhythm)skills[1];
|
||||
var staminaRight = (Stamina)skills[2];
|
||||
var staminaLeft = (Stamina)skills[3];
|
||||
var stamina = (Stamina)skills[2];
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
|
||||
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
|
||||
staminaRating *= staminaPenalty;
|
||||
|
||||
double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
|
||||
//TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance.
|
||||
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05)
|
||||
{
|
||||
staminaPenalty *= 0.25;
|
||||
}
|
||||
|
||||
double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina, staminaPenalty);
|
||||
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
|
||||
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
||||
starRating = rescale(starRating);
|
||||
@ -127,20 +130,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||
/// </remarks>
|
||||
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
|
||||
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina, double staminaPenalty)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||
var staminaRightPeaks = staminaRight.GetCurrentStrainPeaks().ToList();
|
||||
var staminaLeftPeaks = staminaLeft.GetCurrentStrainPeaks().ToList();
|
||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||
|
||||
for (int i = 0; i < colourPeaks.Count; i++)
|
||||
{
|
||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||
double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * staminaPenalty;
|
||||
|
||||
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
|
||||
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0;
|
||||
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= lengthBonus;
|
||||
|
@ -863,5 +863,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
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[3].Type, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyDuplicateInitialCatmullPointIsMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("catmull-duplicate-initial-controlpoint.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
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].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(controlPoints[1].Type, Is.Null);
|
||||
Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -507,7 +507,7 @@ namespace osu.Game.Tests.Database
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.CreateFileSafely(firstFile.File.GetStoragePath()))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
realm.Realm.Write(() =>
|
||||
await realm.Realm.WriteAsync(() =>
|
||||
{
|
||||
foreach (var b in imported.Beatmaps)
|
||||
b.OnlineID = -1;
|
||||
|
@ -59,30 +59,34 @@ namespace osu.Game.Tests.Gameplay
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
|
||||
// No header shouldn't cause any change
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
|
||||
// Reset with a miss instead.
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||
{
|
||||
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
|
||||
});
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
|
||||
// Reset with no judged hit.
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||
{
|
||||
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
|
||||
});
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
|
2
osu.Game.Tests/Resources/adjacent-catmull-segments.osu
Normal file
2
osu.Game.Tests/Resources/adjacent-catmull-segments.osu
Normal file
@ -0,0 +1,2 @@
|
||||
[HitObjects]
|
||||
200,304,23875,6,0,C|288:304|288:304|288:208|288:208|352:208,1,260,8|0
|
@ -0,0 +1,2 @@
|
||||
[HitObjects]
|
||||
200,304,23875,6,0,C|200:304|288:304|288:208|352:208,1,260,8|0
|
@ -5,11 +5,13 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
@ -23,7 +25,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private BindableBeatDivisor bindableBeatDivisor;
|
||||
|
||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType<EquilateralTriangle>().Single();
|
||||
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@ -47,6 +48,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap),
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
|
@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private class SnapProvider : IDistanceSnapProvider
|
||||
{
|
||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||
|
||||
|
@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -13,6 +16,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneEditorClock : EditorClockTestScene
|
||||
{
|
||||
[Cached]
|
||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
|
||||
|
||||
public TestSceneEditorClock()
|
||||
{
|
||||
Add(new FillFlowContainer
|
||||
|
@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneEditorMenuBar : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
public TestSceneEditorMenuBar()
|
||||
{
|
||||
Add(new Container
|
||||
|
@ -12,7 +12,7 @@ using osuTK;
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestScenePlaybackControl : OsuTestScene
|
||||
public class TestScenePlaybackControl : EditorClockTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
48
osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
Normal file
48
osu.Game.Tests/Visual/Editing/TestSceneTapButton.cs
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public class TestSceneTapButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
private TapButton tapButton;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("create button", () =>
|
||||
{
|
||||
Child = tapButton = new TapButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4),
|
||||
};
|
||||
});
|
||||
|
||||
bool pressed = false;
|
||||
|
||||
AddRepeatStep("Press button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(tapButton);
|
||||
if (!pressed)
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
else
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
|
||||
pressed = !pressed;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
132
osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
Normal file
132
osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs
Normal file
@ -0,0 +1,132 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneTapTimingControl : EditorClockTestScene
|
||||
{
|
||||
private EditorBeatmap editorBeatmap => editorBeatmapContainer?.EditorBeatmap;
|
||||
|
||||
private TestSceneHitObjectComposer.EditorBeatmapContainer editorBeatmapContainer;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
[Cached]
|
||||
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
||||
|
||||
private TapTimingControl control;
|
||||
private OsuSpriteText timingInfo;
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create beatmap", () =>
|
||||
{
|
||||
Beatmap.Value = new WaveformTestBeatmap(audio);
|
||||
});
|
||||
|
||||
AddStep("Create component", () =>
|
||||
{
|
||||
Child = editorBeatmapContainer = new TestSceneHitObjectComposer.EditorBeatmapContainer(Beatmap.Value)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 400,
|
||||
Scale = new Vector2(1.5f),
|
||||
Child = control = new TapTimingControl(),
|
||||
},
|
||||
timingInfo = new OsuSpriteText(),
|
||||
}
|
||||
};
|
||||
|
||||
selectedGroup.Value = editorBeatmap.ControlPointInfo.Groups.First();
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (selectedGroup.Value != null)
|
||||
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("set low bpm", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 1000;
|
||||
});
|
||||
|
||||
AddStep("click tap button", () =>
|
||||
{
|
||||
control.ChildrenOfType<OsuButton>()
|
||||
.Last()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddSliderStep("BPM", 30, 400, 128, bpm =>
|
||||
{
|
||||
if (editorBeatmap == null)
|
||||
return;
|
||||
|
||||
editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTapThenReset()
|
||||
{
|
||||
AddStep("click tap button", () =>
|
||||
{
|
||||
control.ChildrenOfType<OsuButton>()
|
||||
.Last()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for track playing", () => Clock.IsRunning);
|
||||
|
||||
AddStep("click reset button", () =>
|
||||
{
|
||||
control.ChildrenOfType<OsuButton>()
|
||||
.First()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,9 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
@ -18,6 +20,28 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer);
|
||||
|
||||
[Test]
|
||||
public void TestContextMenu()
|
||||
{
|
||||
TimelineHitObjectBlueprint blueprint;
|
||||
|
||||
AddStep("add object", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.Add(new HitCircle { StartTime = 3000 });
|
||||
});
|
||||
|
||||
AddStep("click object", () =>
|
||||
{
|
||||
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().Single();
|
||||
InputManager.MoveMouseTo(blueprint);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("context menu open", () => this.ChildrenOfType<OsuContextMenu>().SingleOrDefault()?.State == MenuState.Open);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowZeroDurationObjects()
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@ -14,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
48
osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
Normal file
48
osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public class TestSceneTimelineZoom : TimelineTestScene
|
||||
{
|
||||
public override Drawable CreateTestComponent() => Empty();
|
||||
|
||||
[Test]
|
||||
public void TestVisibleRangeUpdatesOnZoomChange()
|
||||
{
|
||||
double initialVisibleRange = 0;
|
||||
|
||||
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
|
||||
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
||||
|
||||
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
|
||||
AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1));
|
||||
AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
|
||||
AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
|
||||
|
||||
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
|
||||
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisibleRangeConstantOnSizeChange()
|
||||
{
|
||||
double initialVisibleRange = 0;
|
||||
|
||||
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
|
||||
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
|
||||
|
||||
AddStep("scale timeline size", () => TimelineArea.Timeline.Width = 2);
|
||||
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
|
||||
AddStep("descale timeline size", () => TimelineArea.Timeline.Width = 0.5f);
|
||||
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
|
||||
|
||||
AddStep("restore timeline size", () => TimelineArea.Timeline.Width = 1);
|
||||
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private TimingScreen timingScreen;
|
||||
|
||||
protected override bool ScrollUsingMouseWheel => false;
|
||||
|
||||
public TestSceneTimingScreen()
|
||||
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||
Beatmap.Disabled = true;
|
||||
|
||||
Child = new TimingScreen
|
||||
Child = timingScreen = new TimingScreen
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
};
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Stop clock", () => Clock.Stop());
|
||||
|
||||
AddUntilStep("wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackingCurrentTimeWhileRunning()
|
||||
{
|
||||
AddStep("Select first effect point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||
|
||||
AddStep("Seek to just before next point", () => Clock.Seek(69000));
|
||||
AddStep("Start clock", () => Clock.Start());
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackingCurrentTimeWhilePaused()
|
||||
{
|
||||
AddStep("Select first effect point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||
|
||||
AddStep("Seek to later", () => Clock.Seek(80000));
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
|
@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = OsuColour.Gray(30)
|
||||
},
|
||||
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
|
||||
scrollContainer = new ZoomableScrollContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
},
|
||||
new MenuCursor()
|
||||
@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestWidthInitialization()
|
||||
{
|
||||
AddAssert("Inner container width was initialized", () => innerBox.DrawWidth > 0);
|
||||
AddAssert("Inner container width was initialized", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWidthUpdatesOnDrawSizeChanges()
|
||||
{
|
||||
AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f);
|
||||
AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2);
|
||||
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -38,25 +39,29 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
|
||||
|
||||
AddRange(new Drawable[]
|
||||
Add(new OsuContextMenuContainer
|
||||
{
|
||||
EditorBeatmap,
|
||||
Composer,
|
||||
new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
EditorBeatmap,
|
||||
Composer,
|
||||
new FillFlowContainer
|
||||
{
|
||||
new StartStopButton(),
|
||||
new AudioVisualiser(),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StartStopButton(),
|
||||
new AudioVisualiser(),
|
||||
}
|
||||
},
|
||||
TimelineArea = new TimelineArea(CreateTestComponent())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
},
|
||||
TimelineArea = new TimelineArea(CreateTestComponent())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
@ -22,7 +21,6 @@ using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -33,18 +31,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Resolved]
|
||||
private SkinManager skinManager { get; set; }
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
@ -81,11 +67,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
if (expectedComponentsContainer == null)
|
||||
return false;
|
||||
|
||||
var expectedComponentsAdjustmentContainer = new Container
|
||||
var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer
|
||||
{
|
||||
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
|
||||
Size = actualComponentsContainer.DrawSize,
|
||||
Child = expectedComponentsContainer,
|
||||
// proxy the same required dependencies that `actualComponentsContainer` is using.
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get<ScoreProcessor>()),
|
||||
(typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get<HealthProcessor>()),
|
||||
(typeof(GameplayState), actualComponentsContainer.Dependencies.Get<GameplayState>()),
|
||||
(typeof(GameplayClock), actualComponentsContainer.Dependencies.Get<GameplayClock>())
|
||||
},
|
||||
};
|
||||
|
||||
Add(expectedComponentsAdjustmentContainer);
|
||||
|
@ -15,7 +15,7 @@ using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
@ -14,16 +13,15 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -41,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private TestReplayRecorder recorder;
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
@ -16,7 +16,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
[Cached]
|
||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
@ -18,8 +18,8 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
using osuTK;
|
||||
@ -259,12 +259,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestFinalFramesPurgedBeforeEndingPlay()
|
||||
{
|
||||
AddStep("begin playing", () => spectatorClient.BeginPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()), new Score()));
|
||||
AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
|
||||
|
||||
AddStep("send frames and finish play", () =>
|
||||
{
|
||||
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
|
||||
spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true });
|
||||
|
||||
var completedGameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||
completedGameplayState.HasPassed = true;
|
||||
spectatorClient.EndPlaying(completedGameplayState);
|
||||
});
|
||||
|
||||
// We can't access API because we're an "online" test.
|
||||
|
@ -20,13 +20,13 @@ using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Mods;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
using osuTK;
|
||||
@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
CachedDependencies = new[]
|
||||
{
|
||||
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
|
||||
(typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>()))
|
||||
(typeof(GameplayState), TestGameplayState.Create(new OsuRuleset()))
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
@ -124,13 +124,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Status = { Value = new RoomStatusOpen() },
|
||||
Category = { Value = RoomCategory.Spotlight },
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = { Value = "Featured artist room" },
|
||||
Status = { Value = new RoomStatusOpen() },
|
||||
Category = { Value = RoomCategory.FeaturedArtist },
|
||||
}),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 5);
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 6);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -73,19 +72,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click select all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<SelectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
private void createFreeModSelect()
|
||||
|
@ -1,6 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -15,13 +16,14 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@ -349,15 +351,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests spectating with a gameplay start time set to a negative value.
|
||||
/// Simulating beatmaps with high <see cref="BeatmapInfo.AudioLeadIn"/> or negative time storyboard elements.
|
||||
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNegativeGameplayStartTime()
|
||||
public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000);
|
||||
|
||||
/// <summary>
|
||||
/// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestIntroStoryboardElement() => testLeadIn(b =>
|
||||
{
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1);
|
||||
b.Storyboard.GetLayer("Background").Add(sprite);
|
||||
});
|
||||
|
||||
private void testLeadIn(Action<WorkingBeatmap> applyToBeatmap = null)
|
||||
{
|
||||
start(PLAYER_1_ID);
|
||||
|
||||
loadSpectateScreen(false, -500);
|
||||
loadSpectateScreen(false, applyToBeatmap);
|
||||
|
||||
// to ensure negative gameplay start time does not affect spectator, send frames exactly after StartGameplay().
|
||||
// (similar to real spectating sessions in which the first frames get sent between StartGameplay() and player load complete)
|
||||
@ -371,14 +385,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertRunning(PLAYER_1_ID);
|
||||
}
|
||||
|
||||
private void loadSpectateScreen(bool waitForPlayerLoad = true, double? gameplayStartTime = null)
|
||||
private void loadSpectateScreen(bool waitForPlayerLoad = true, Action<WorkingBeatmap> applyToBeatmap = null)
|
||||
{
|
||||
AddStep(!gameplayStartTime.HasValue ? "load screen" : $"load screen (start = {gameplayStartTime}ms)", () =>
|
||||
AddStep("load screen", () =>
|
||||
{
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
|
||||
Ruleset.Value = importedBeatmap.Ruleset;
|
||||
|
||||
LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(SelectedRoom.Value, playingUsers.ToArray(), gameplayStartTime));
|
||||
applyToBeatmap?.Invoke(Beatmap.Value);
|
||||
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value, playingUsers.ToArray()));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
|
||||
@ -461,19 +477,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
|
||||
|
||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||
|
||||
private class TestMultiSpectatorScreen : MultiSpectatorScreen
|
||||
{
|
||||
private readonly double? startTime;
|
||||
|
||||
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
|
||||
: base(room, users)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
|
||||
=> new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
@ -28,6 +29,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@ -178,6 +180,47 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectKeyWithAllowedMods()
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) }
|
||||
});
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
|
||||
|
||||
AddUntilStep("mod select shown", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectKeyWithNoAllowedMods()
|
||||
{
|
||||
AddStep("add playlist item with no allowed mods", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
});
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
|
||||
|
||||
AddWaitStep("wait some", 3);
|
||||
AddAssert("mod select not shown", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNextPlaylistItemSelectedAfterCompletion()
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using NUnit.Framework;
|
||||
@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
if (isDisposing)
|
||||
return;
|
||||
|
||||
using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create))
|
||||
using (var outStream = LocalStorage.CreateFileSafely(DatabaseContextFactory.DATABASE_NAME))
|
||||
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
|
||||
stream.CopyTo(outStream);
|
||||
}
|
||||
|
@ -8,9 +8,11 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
@ -54,6 +56,39 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaEscapeAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSongSelectBackActionHandling()
|
||||
{
|
||||
TestPlaySongSelect songSelect = null;
|
||||
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
|
||||
AddStep("set filter", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
|
||||
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
|
||||
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("open collections dropdown", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionFilterDropdown>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("collections dropdown closed", () => songSelect
|
||||
.ChildrenOfType<CollectionFilterDropdown>().Single()
|
||||
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
||||
|
||||
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
|
||||
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
|
||||
ConfirmAtMainMenu();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
|
||||
/// but should be handled *after* song select).
|
||||
@ -487,6 +522,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
|
||||
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
// move the mouse firmly inside game bounds to avoid interfering with other tests.
|
||||
AddStep("center cursor", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -10,7 +10,10 @@ using osu.Game.Rulesets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
@ -101,6 +104,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet()));
|
||||
downloadAssert(true);
|
||||
|
||||
AddAssert("status is loved", () => overlay.ChildrenOfType<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Loved);
|
||||
AddAssert("scores container is visible", () => overlay.ChildrenOfType<ScoresContainer>().Single().Alpha == 1);
|
||||
|
||||
AddStep("go to second beatmap", () => overlay.ChildrenOfType<BeatmapPicker.DifficultySelectorButton>().ElementAt(1).TriggerClick());
|
||||
|
||||
AddAssert("status is graveyard", () => overlay.ChildrenOfType<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Graveyard);
|
||||
AddAssert("scores container is hidden", () => overlay.ChildrenOfType<ScoresContainer>().Single().Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -232,6 +243,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
|
||||
},
|
||||
Status = i % 2 == 0 ? BeatmapOnlineStatus.Graveyard : BeatmapOnlineStatus.Loved,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osu.Game.Overlays.Chat.Listing;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
||||
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
||||
selected.Value = null;
|
||||
selected.Value = channelList.ChannelListingChannel;
|
||||
channelList.RemoveChannel(channel);
|
||||
};
|
||||
|
||||
@ -111,6 +112,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
for (int i = 0; i < 10; i++)
|
||||
channelList.AddChannel(createRandomPrivateChannel());
|
||||
});
|
||||
|
||||
AddStep("Add Announce Channels", () =>
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
channelList.AddChannel(createRandomAnnounceChannel());
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -118,35 +125,37 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("Unread Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Unread.Value = true;
|
||||
});
|
||||
|
||||
AddStep("Read Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Unread.Value = false;
|
||||
});
|
||||
|
||||
AddStep("Add Mention Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value++;
|
||||
});
|
||||
|
||||
AddStep("Add 98 Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value += 98;
|
||||
});
|
||||
|
||||
AddStep("Clear Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private bool validItem => selected.Value != null && !(selected.Value is ChannelListing.ChannelListingChannel);
|
||||
|
||||
private Channel createRandomPublicChannel()
|
||||
{
|
||||
int id = RNG.Next(0, 10000);
|
||||
@ -167,5 +176,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Username = $"test user {id}",
|
||||
});
|
||||
}
|
||||
|
||||
private Channel createRandomAnnounceChannel()
|
||||
{
|
||||
int id = RNG.Next(0, 10000);
|
||||
return new Channel
|
||||
{
|
||||
Name = $"Announce {id}",
|
||||
Type = ChannelType.Announce,
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,129 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays.Chat.Tabs;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public class TestSceneChannelTabControl : OsuTestScene
|
||||
{
|
||||
private readonly TestTabControl channelTabControl;
|
||||
|
||||
public TestSceneChannelTabControl()
|
||||
{
|
||||
SpriteText currentText;
|
||||
Add(new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
channelTabControl = new TestTabControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Height = 50
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.1f),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 50,
|
||||
Depth = -1,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Add(new Container
|
||||
{
|
||||
Origin = Anchor.TopLeft,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
currentText = new OsuSpriteText
|
||||
{
|
||||
Text = "Currently selected channel:"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
|
||||
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue;
|
||||
|
||||
AddStep("Add random private channel", addRandomPrivateChannel);
|
||||
AddAssert("There is only one channels", () => channelTabControl.Items.Count == 2);
|
||||
AddRepeatStep("Add 3 random private channels", addRandomPrivateChannel, 3);
|
||||
AddAssert("There are four channels", () => channelTabControl.Items.Count == 5);
|
||||
AddStep("Add random public channel", () => addChannel(RNG.Next().ToString()));
|
||||
|
||||
AddRepeatStep("Select a random channel", () =>
|
||||
{
|
||||
List<Channel> validChannels = channelTabControl.Items.Where(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)).ToList();
|
||||
channelTabControl.SelectChannel(validChannels[RNG.Next(0, validChannels.Count)]);
|
||||
}, 20);
|
||||
|
||||
Channel channelBefore = null;
|
||||
AddStep("set first channel", () => channelTabControl.SelectChannel(channelBefore = channelTabControl.Items.First(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel))));
|
||||
|
||||
AddStep("select selector tab", () => channelTabControl.SelectChannel(channelTabControl.Items.Single(c => c is ChannelSelectorTabItem.ChannelSelectorTabChannel)));
|
||||
AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
|
||||
|
||||
AddAssert("check channel unchanged", () => channelBefore == channelTabControl.Current.Value);
|
||||
|
||||
AddStep("set second channel", () => channelTabControl.SelectChannel(channelTabControl.Items.GetNext(channelBefore)));
|
||||
AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value);
|
||||
|
||||
AddUntilStep("remove all channels", () =>
|
||||
{
|
||||
foreach (var item in channelTabControl.Items.ToList())
|
||||
{
|
||||
if (item is ChannelSelectorTabItem.ChannelSelectorTabChannel)
|
||||
continue;
|
||||
|
||||
channelTabControl.RemoveChannel(item);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
|
||||
}
|
||||
|
||||
private void addRandomPrivateChannel() =>
|
||||
channelTabControl.AddChannel(new Channel(new APIUser
|
||||
{
|
||||
Id = RNG.Next(1000, 10000000),
|
||||
Username = "Test User " + RNG.Next(1000)
|
||||
}));
|
||||
|
||||
private void addChannel(string name) =>
|
||||
channelTabControl.AddChannel(new Channel
|
||||
{
|
||||
Type = ChannelType.Public,
|
||||
Name = name
|
||||
});
|
||||
|
||||
private class TestTabControl : ChannelTabControl
|
||||
{
|
||||
public void SelectChannel(Channel channel) => base.SelectTab(TabMap[channel]);
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -22,12 +21,10 @@ namespace osu.Game.Tests.Visual.Online
|
||||
public class TestSceneChatLink : OsuTestScene
|
||||
{
|
||||
private readonly TestChatLineContainer textContainer;
|
||||
private readonly DialogOverlay dialogOverlay;
|
||||
private Color4 linkColour;
|
||||
|
||||
public TestSceneChatLink()
|
||||
{
|
||||
Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue });
|
||||
Add(textContainer = new TestChatLineContainer
|
||||
{
|
||||
Padding = new MarginPadding { Left = 20, Right = 20 },
|
||||
@ -47,9 +44,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
availableChannels.Add(new Channel { Name = "#english" });
|
||||
availableChannels.Add(new Channel { Name = "#japanese" });
|
||||
Dependencies.Cache(chatManager);
|
||||
|
||||
Dependencies.Cache(new ChatOverlay());
|
||||
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,436 +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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Overlays.Chat.Listing;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
|
||||
{
|
||||
private ChatOverlayV2 chatOverlay;
|
||||
private ChannelManager channelManager;
|
||||
|
||||
private APIUser testUser;
|
||||
private Channel testPMChannel;
|
||||
private Channel[] testChannels;
|
||||
|
||||
private Channel testChannel1 => testChannels[0];
|
||||
private Channel testChannel2 => testChannels[1];
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
testUser = new APIUser { Username = "test user", Id = 5071479 };
|
||||
testPMChannel = new Channel(testUser);
|
||||
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
|
||||
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(ChannelManager), channelManager = new ChannelManager()),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
channelManager,
|
||||
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Setup request handler", () =>
|
||||
{
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetUpdatesRequest getUpdates:
|
||||
getUpdates.TriggerFailure(new WebException());
|
||||
return true;
|
||||
|
||||
case JoinChannelRequest joinChannel:
|
||||
joinChannel.TriggerSuccess();
|
||||
return true;
|
||||
|
||||
case LeaveChannelRequest leaveChannel:
|
||||
leaveChannel.TriggerSuccess();
|
||||
return true;
|
||||
|
||||
case GetMessagesRequest getMessages:
|
||||
getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
|
||||
return true;
|
||||
|
||||
case GetUserRequest getUser:
|
||||
if (getUser.Lookup == testUser.Username)
|
||||
getUser.TriggerSuccess(testUser);
|
||||
else
|
||||
getUser.TriggerFailure(new WebException());
|
||||
return true;
|
||||
|
||||
case PostMessageRequest postMessage:
|
||||
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
|
||||
{
|
||||
Content = postMessage.Message.Content,
|
||||
ChannelId = postMessage.Message.ChannelId,
|
||||
Sender = postMessage.Message.Sender,
|
||||
Timestamp = new DateTimeOffset(DateTime.Now),
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
Logger.Log($"Unhandled Request Type: {req.GetType()}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("Add test channels", () =>
|
||||
{
|
||||
(channelManager.AvailableChannels as BindableList<Channel>)?.AddRange(testChannels);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("Show overlay with channel", () =>
|
||||
{
|
||||
chatOverlay.Show();
|
||||
Channel joinedChannel = channelManager.JoinChannel(testChannel1);
|
||||
channelManager.CurrentChannel.Value = joinedChannel;
|
||||
});
|
||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||
AddUntilStep("Channel is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowHide()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
||||
AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChatHeight()
|
||||
{
|
||||
BindableFloat configChatHeight = new BindableFloat();
|
||||
config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight);
|
||||
float newHeight = 0;
|
||||
|
||||
AddStep("Reset config chat height", () => configChatHeight.SetDefault());
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default);
|
||||
AddStep("Click top bar", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(chatOverlayTopBar);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)));
|
||||
AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddStep("Store new height", () => newHeight = chatOverlay.Height);
|
||||
AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight);
|
||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChannelSelection()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("Listing is visible", () => listingIsVisible);
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearchInListing()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("Listing is visible", () => listingIsVisible);
|
||||
AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
|
||||
AddUntilStep("Only channel 2 visibile", () =>
|
||||
{
|
||||
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
|
||||
.Where(item => item.IsPresent);
|
||||
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChannelCloseButton()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join PM and public channels", () =>
|
||||
{
|
||||
channelManager.JoinChannel(testChannel1);
|
||||
channelManager.JoinChannel(testPMChannel);
|
||||
});
|
||||
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
|
||||
AddStep("Click close button", () =>
|
||||
{
|
||||
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
|
||||
clickDrawable(closeButton);
|
||||
});
|
||||
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
|
||||
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Click close button", () =>
|
||||
{
|
||||
ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType<ChannelListItemCloseButton>().Single();
|
||||
clickDrawable(closeButton);
|
||||
});
|
||||
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChatCommand()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
|
||||
AddAssert("PM channel is selected", () =>
|
||||
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
|
||||
AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
|
||||
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
|
||||
|
||||
// Make sure no unnecessary requests are made when the PM channel is already open.
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
|
||||
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
|
||||
AddAssert("PM channel is selected", () =>
|
||||
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiplayerChannelIsNotShown()
|
||||
{
|
||||
Channel multiplayerChannel = null;
|
||||
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
|
||||
{
|
||||
Name = "#mp_1",
|
||||
Type = ChannelType.Multiplayer,
|
||||
}));
|
||||
AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
|
||||
AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType<ChannelListingItem>()
|
||||
.Where(item => item.IsPresent)
|
||||
.Select(item => item.Channel)
|
||||
.Contains(multiplayerChannel));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightOnCurrentChannel()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Send message in channel 1", () =>
|
||||
{
|
||||
testChannel1.AddNewMessages(message = new Message
|
||||
{
|
||||
ChannelId = testChannel1.Id,
|
||||
Content = "Message to highlight!",
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Sender = testUser,
|
||||
});
|
||||
});
|
||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightOnAnotherChannel()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Send message in channel 2", () =>
|
||||
{
|
||||
testChannel2.AddNewMessages(message = new Message
|
||||
{
|
||||
ChannelId = testChannel2.Id,
|
||||
Content = "Message to highlight!",
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Sender = testUser,
|
||||
});
|
||||
});
|
||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
|
||||
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightOnLeftChannel()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddStep("Send message in channel 2", () =>
|
||||
{
|
||||
testChannel2.AddNewMessages(message = new Message
|
||||
{
|
||||
ChannelId = testChannel2.Id,
|
||||
Content = "Message to highlight!",
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Sender = testUser,
|
||||
});
|
||||
});
|
||||
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
|
||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
|
||||
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightWhileChatNeverOpen()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Send message in channel 1", () =>
|
||||
{
|
||||
testChannel1.AddNewMessages(message = new Message
|
||||
{
|
||||
ChannelId = testChannel1.Id,
|
||||
Content = "Message to highlight!",
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Sender = testUser,
|
||||
});
|
||||
});
|
||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightWithNullChannel()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Send message in channel 1", () =>
|
||||
{
|
||||
testChannel1.AddNewMessages(message = new Message
|
||||
{
|
||||
ChannelId = testChannel1.Id,
|
||||
Content = "Message to highlight!",
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
Sender = testUser,
|
||||
});
|
||||
});
|
||||
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
|
||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TextBoxRetainsFocus()
|
||||
{
|
||||
AddStep("Show overlay", () => chatOverlay.Show());
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListSelector>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
|
||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
||||
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
|
||||
}
|
||||
|
||||
private bool listingIsVisible =>
|
||||
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
|
||||
|
||||
private bool loadingIsVisible =>
|
||||
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value == Visibility.Visible;
|
||||
|
||||
private bool channelIsVisible =>
|
||||
!listingIsVisible && !loadingIsVisible;
|
||||
|
||||
private DrawableChannel currentDrawableChannel =>
|
||||
chatOverlay.ChildrenOfType<DrawableChannel>().Single();
|
||||
|
||||
private ChannelListItem getChannelListItem(Channel channel) =>
|
||||
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
|
||||
|
||||
private ChatTextBox chatOverlayTextBox =>
|
||||
chatOverlay.ChildrenOfType<ChatTextBox>().Single();
|
||||
|
||||
private ChatOverlayTopBar chatOverlayTopBar =>
|
||||
chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single();
|
||||
|
||||
private void clickDrawable(Drawable d)
|
||||
{
|
||||
InputManager.MoveMouseTo(d);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
|
||||
private List<Message> createChannelMessages(Channel channel)
|
||||
{
|
||||
var message = new Message
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = $"Hello, this is a message in {channel.Name}",
|
||||
Sender = testUser,
|
||||
Timestamp = new DateTimeOffset(DateTime.Now),
|
||||
};
|
||||
return new List<Message> { message };
|
||||
}
|
||||
|
||||
private Channel createPublicChannel(int id) => new Channel
|
||||
{
|
||||
Id = id,
|
||||
Name = $"#channel-{id}",
|
||||
Topic = $"We talk about the number {id} here",
|
||||
Type = ChannelType.Public,
|
||||
};
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dashboard;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
using osu.Game.Users;
|
||||
@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(SpectatorClient), spectatorClient),
|
||||
(typeof(UserLookupCache), lookupCache)
|
||||
(typeof(UserLookupCache), lookupCache),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
|
||||
},
|
||||
Child = currentlyPlaying = new CurrentlyPlayingDisplay
|
||||
{
|
||||
|
@ -128,11 +128,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddAssert("Ensure no adjacent day separators", () =>
|
||||
{
|
||||
var indices = chatDisplay.FillFlow.OfType<DrawableChannel.DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
|
||||
var indices = chatDisplay.FillFlow.OfType<DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
|
||||
|
||||
foreach (int i in indices)
|
||||
{
|
||||
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
|
||||
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DaySeparator)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
AddUntilStep("wait for scores loaded", () =>
|
||||
requestComplete
|
||||
// request handler may need to fire more than once to get scores.
|
||||
&& totalCount > 0
|
||||
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
||||
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
||||
AddWaitStep("wait for display", 5);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -13,6 +14,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@ -114,10 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap);
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap)
|
||||
{
|
||||
@ -151,6 +150,24 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private class TestBeatmapConverter : IBeatmapConverter
|
||||
{
|
||||
#pragma warning disable CS0067 // The event is never used
|
||||
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
public TestBeatmapConverter(IBeatmap beatmap)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
public bool CanConvert() => true;
|
||||
|
||||
public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
|
||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddStep("clear label", () => textBox.LabelText = default);
|
||||
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||
|
||||
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
|
||||
AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
|
||||
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||
}
|
||||
|
||||
@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
SettingsNumberBox numberBox = null;
|
||||
|
||||
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
|
||||
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<SettingsNoticeText>().Any());
|
||||
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
|
||||
|
||||
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
|
||||
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
||||
AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
|
||||
AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
|
||||
|
||||
AddStep("unset warning text", () => numberBox.WarningText = default);
|
||||
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
|
||||
AddStep("unset warning text", () => numberBox.ClearNoticeText());
|
||||
AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
|
||||
|
||||
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
|
||||
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
||||
AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
|
||||
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
|
||||
|
||||
AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
.OfType<ISettingsItem>()
|
||||
.OfType<IFilterable>()
|
||||
.Where(f => !(f is IHasFilterableChildren))
|
||||
.All(f => f.FilterTerms.Any(t => t.Contains("scaling")))
|
||||
.All(f => f.FilterTerms.Any(t => t.ToString().Contains("scaling")))
|
||||
));
|
||||
|
||||
AddAssert("ensure section is current", () => settings.CurrentSection.Value is GraphicsSection);
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
@ -127,6 +126,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNullBeatmap()
|
||||
{
|
||||
@ -147,24 +152,48 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestBPMUpdates()
|
||||
{
|
||||
const float bpm = 120;
|
||||
const double bpm = 120;
|
||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||
|
||||
OsuModDoubleTime doubleTime = null;
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM(bpm);
|
||||
checkDisplayedBPM($"{bpm}");
|
||||
|
||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
||||
checkDisplayedBPM(bpm * 1.5f);
|
||||
checkDisplayedBPM($"{bpm * 1.5f}");
|
||||
|
||||
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||
checkDisplayedBPM(bpm * 2);
|
||||
checkDisplayedBPM($"{bpm * 2}");
|
||||
}
|
||||
|
||||
void checkDisplayedBPM(float target) =>
|
||||
AddUntilStep($"displayed bpm is {target}", () => this.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any(
|
||||
label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture)));
|
||||
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||
[TestCase(120, 120.4, null, "120")]
|
||||
[TestCase(120, 120.6, "DT", "180-182 (mostly 180)")]
|
||||
[TestCase(120, 120.4, "DT", "180")]
|
||||
public void TestVaryingBPM(double commonBpm, double otherBpm, string mod, string expectedDisplay)
|
||||
{
|
||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
beatmap.ControlPointInfo.Add(100, new TimingControlPoint { BeatLength = 60 * 1000 / otherBpm });
|
||||
beatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 60 * 1000 / commonBpm });
|
||||
|
||||
if (mod != null)
|
||||
AddStep($"select {mod}", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateModFromAcronym(mod) });
|
||||
|
||||
selectBeatmap(beatmap);
|
||||
checkDisplayedBPM(expectedDisplay);
|
||||
}
|
||||
|
||||
private void checkDisplayedBPM(string target)
|
||||
{
|
||||
AddUntilStep($"displayed bpm is {target}", () =>
|
||||
{
|
||||
var label = infoWedge.DisplayedContent.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Single(l => l.Statistic.Name == "BPM");
|
||||
return label.Statistic.Content == target;
|
||||
});
|
||||
}
|
||||
|
||||
private void setRuleset(RulesetInfo rulesetInfo)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio.Track;
|
||||
@ -82,11 +83,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
if (!allowMistimed)
|
||||
{
|
||||
AddAssert("trigger is near beat length", () => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
|
||||
AddAssert("trigger is near beat length",
|
||||
() => lastActuationTime != null && lastBeatIndex != null && Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value,
|
||||
BeatSyncedContainer.MISTIMED_ALLOWANCE));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAssert("trigger is not near beat length", () => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength, lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
|
||||
AddAssert("trigger is not near beat length",
|
||||
() => lastActuationTime != null && lastBeatIndex != null && !Precision.AlmostEquals(lastTimingPoint.Time + lastBeatIndex.Value * lastTimingPoint.BeatLength,
|
||||
lastActuationTime.Value, BeatSyncedContainer.MISTIMED_ALLOWANCE));
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,24 +263,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Beatmap.BindValueChanged(_ =>
|
||||
{
|
||||
timingPointCount.Value = 0;
|
||||
currentTimingPoint.Value = 0;
|
||||
beatCount.Value = 0;
|
||||
currentBeat.Value = 0;
|
||||
beatsPerMinute.Value = 0;
|
||||
adjustedBeatLength.Value = 0;
|
||||
timeUntilNextBeat.Value = 0;
|
||||
timeSinceLastBeat.Value = 0;
|
||||
}, true);
|
||||
}
|
||||
|
||||
private List<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
|
||||
private List<TimingControlPoint> timingPoints => BeatSyncSource.ControlPoints?.TimingPoints.ToList();
|
||||
|
||||
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
|
||||
{
|
||||
@ -292,7 +280,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
if (timingPoints.Count == 0) return 0;
|
||||
|
||||
if (timingPoints[^1] == current)
|
||||
return (int)Math.Ceiling((BeatSyncClock.CurrentTime - current.Time) / current.BeatLength);
|
||||
{
|
||||
Debug.Assert(BeatSyncSource.Clock != null);
|
||||
|
||||
return (int)Math.Ceiling((BeatSyncSource.Clock.CurrentTime - current.Time) / current.BeatLength);
|
||||
}
|
||||
|
||||
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
|
||||
}
|
||||
@ -300,9 +292,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Debug.Assert(BeatSyncSource.Clock != null);
|
||||
|
||||
timeUntilNextBeat.Value = TimeUntilNextBeat;
|
||||
timeSinceLastBeat.Value = TimeSinceLastBeat;
|
||||
currentTime.Value = BeatSyncClock.CurrentTime;
|
||||
currentTime.Value = BeatSyncSource.Clock.CurrentTime;
|
||||
}
|
||||
|
||||
public Action<int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes> NewBeat;
|
||||
|
@ -0,0 +1,48 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.FirstRunSetup;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneFirstRunScreenImportFromStable : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
private readonly Mock<LegacyImportManager> legacyImportManager = new Mock<LegacyImportManager>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
legacyImportManager.Setup(m => m.GetImportCount(It.IsAny<StableContent>(), It.IsAny<CancellationToken>())).Returns(() => Task.FromResult(RNG.Next(0, 256)));
|
||||
|
||||
Dependencies.CacheAs(legacyImportManager.Object);
|
||||
}
|
||||
|
||||
public TestSceneFirstRunScreenImportFromStable()
|
||||
{
|
||||
AddStep("load screen", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ScreenStack(new ScreenImportFromStable())
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("step to next", () => overlay.NextButton.TriggerClick());
|
||||
|
||||
AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenBeatmaps);
|
||||
AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale);
|
||||
|
||||
AddStep("hide", () => overlay.Hide());
|
||||
AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden);
|
||||
@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("run notification action", () => lastNotification.Activated());
|
||||
|
||||
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
|
||||
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenBeatmaps);
|
||||
AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale);
|
||||
}
|
||||
|
||||
// interface mocks break hot reload, mocking this stub implementation instead works around it.
|
||||
|
@ -435,15 +435,19 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
|
||||
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
||||
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("results filtered correctly",
|
||||
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
|
||||
.Where(item => item.MatchingFilter)
|
||||
.All(item => item.FilterTerms.Any(term => term.Contains("10"))));
|
||||
.All(item => item.FilterTerms.Any(term => term.ToString().Contains("10"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,12 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@ -15,14 +18,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
private readonly BindableBool enabled = new BindableBool(true);
|
||||
|
||||
protected override Drawable CreateContent() => new RoundedButton
|
||||
protected override Drawable CreateContent()
|
||||
{
|
||||
Width = 400,
|
||||
Text = "Test button",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Enabled = { BindTarget = enabled },
|
||||
};
|
||||
return new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
{
|
||||
Width = 400,
|
||||
Text = "Test button",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Enabled = { BindTarget = enabled },
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = "Test button",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Enabled = { BindTarget = enabled },
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisabled()
|
||||
@ -34,7 +54,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public void TestBackgroundColour()
|
||||
{
|
||||
AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
|
||||
AddAssert("first button has correct colour", () => Cell(0, 1).ChildrenOfType<RoundedButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
|
||||
AddAssert("rounded button has correct colour", () => Cell(0, 1).ChildrenOfType<RoundedButton>().First().BackgroundColour == new OsuColour().Blue3);
|
||||
AddAssert("settings button has correct colour", () => Cell(0, 1).ChildrenOfType<SettingsButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Highlight1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,13 @@ namespace osu.Game.Tests
|
||||
|
||||
protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
|
||||
|
||||
public override bool TryTransferTrack(WorkingBeatmap target)
|
||||
{
|
||||
// Our track comes from a local track store that's disposed on finalizer,
|
||||
// therefore it's unsafe to transfer it to another working beatmap.
|
||||
return false;
|
||||
}
|
||||
|
||||
private string firstAudioFile
|
||||
{
|
||||
get
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
||||
|
||||
using (var stream = storage.GetStream("bracket.json", FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.CreateFileSafely("bracket.json"))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.Write(@"{
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Tests.Screens
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Storage storage)
|
||||
{
|
||||
using (var stream = storage.GetStream("drawings.txt", FileAccess.Write))
|
||||
using (var stream = storage.CreateFileSafely("drawings.txt"))
|
||||
using (var writer = new StreamWriter(stream))
|
||||
{
|
||||
writer.WriteLine("KR : South Korea : KOR");
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Models
|
||||
|
||||
public void SaveChanges()
|
||||
{
|
||||
using (var stream = configStorage.GetStream(config_path, FileAccess.Write, FileMode.Create))
|
||||
using (var stream = configStorage.CreateFileSafely(config_path))
|
||||
using (var sw = new StreamWriter(stream))
|
||||
{
|
||||
sw.Write(JsonConvert.SerializeObject(this,
|
||||
|
@ -205,7 +205,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
||||
try
|
||||
{
|
||||
// Write to drawings_results
|
||||
using (Stream stream = storage.GetStream(results_filename, FileAccess.Write, FileMode.Create))
|
||||
using (Stream stream = storage.CreateFileSafely(results_filename))
|
||||
using (StreamWriter sw = new StreamWriter(stream))
|
||||
{
|
||||
sw.Write(text);
|
||||
|
@ -259,7 +259,7 @@ namespace osu.Game.Tournament
|
||||
|
||||
public void PopulateUser(APIUser user, Action success = null, Action failure = null, bool immediate = false)
|
||||
{
|
||||
var req = new GetUserRequest(user.Id, Ruleset.Value);
|
||||
var req = new GetUserRequest(user.Id, ladder.Ruleset.Value);
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
@ -321,7 +321,7 @@ namespace osu.Game.Tournament
|
||||
Converters = new JsonConverter[] { new JsonPointConverter() }
|
||||
});
|
||||
|
||||
using (var stream = storage.GetStream(BRACKET_FILENAME, FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
|
||||
using (var sw = new StreamWriter(stream))
|
||||
sw.Write(serialisedLadder);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 enable
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user