mirror of
https://github.com/osukey/osukey.git
synced 2025-08-06 16:13:57 +09:00
Merge branch 'master' into delete-all-beatmap-videos-sbs
This commit is contained in:
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/inspectcode
|
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
|
- name: Dotnet code style
|
||||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
||||||
@ -78,15 +78,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: "6.0.x"
|
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
|
- name: Compile
|
||||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
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/
|
SENTRY_URL: https://sentry.ppy.sh/
|
||||||
with:
|
with:
|
||||||
environment: production
|
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
|
# Fody (pulled in by Realm) - schema file
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
**/FodyWeavers.xml
|
**/FodyWeavers.xml
|
||||||
|
|
||||||
|
.idea/.idea.osu.Desktop/.idea/misc.xml
|
11
.idea/.idea.osu.Desktop/.idea/misc.xml
generated
11
.idea/.idea.osu.Desktop/.idea/misc.xml
generated
@ -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.
|
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: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: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.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: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.
|
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" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.511.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.605.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- 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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -18,5 +18,6 @@
|
|||||||
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
||||||
<file src="**.dll" target="lib\net45\"/>
|
<file src="**.dll" target="lib\net45\"/>
|
||||||
<file src="**.config" target="lib\net45\"/>
|
<file src="**.config" target="lib\net45\"/>
|
||||||
|
<file src="**.json" target="lib\net45\"/>
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||||
{
|
{
|
||||||
private const double velocity = 0.5;
|
private const double velocity_factor = 0.5;
|
||||||
|
|
||||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var playable = base.GetPlayableBeatmap();
|
var playable = base.GetPlayableBeatmap();
|
||||||
playable.Difficulty.SliderTickRate = 5;
|
playable.Difficulty.SliderTickRate = 5;
|
||||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||||
return playable;
|
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("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||||
|
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVelocityLimit()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300 };
|
double[] times = { 100, 300 };
|
||||||
float[] positions = { 200, 500 };
|
float[] positions = { 200, 500 };
|
||||||
addPlacementSteps(times, positions);
|
addPlacementSteps(times, positions);
|
||||||
addPathCheckStep(times, new float[] { 200, 300 });
|
addPathCheckStep(times, positions);
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestClampedPositionIsRestored()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300, 500 };
|
double[] times = { 100, 300, 500 };
|
||||||
float[] positions = { 200, 200, 0, 250 };
|
float[] positions = { 200, 200, -3000, 250 };
|
||||||
|
|
||||||
addMoveAndClickSteps(times[0], positions[0]);
|
addMoveAndClickSteps(times[0], positions[0]);
|
||||||
addMoveAndClickSteps(times[1], positions[1]);
|
addMoveAndClickSteps(times[1], positions[1]);
|
||||||
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
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]
|
[Test]
|
||||||
public void TestOutOfOrder()
|
public void TestOutOfOrder()
|
||||||
{
|
{
|
||||||
|
@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClampedPositionIsRestored()
|
public void TestSliderVelocityChange()
|
||||||
{
|
{
|
||||||
const double velocity = 0.25;
|
double[] times = { 100, 300 };
|
||||||
double[] times = { 100, 500, 700 };
|
float[] positions = { 200, 300 };
|
||||||
float[] positions = { 100, 100, 100 };
|
addBlueprintStep(times, positions);
|
||||||
addBlueprintStep(times, positions, velocity);
|
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
|
|
||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddMouseMoveStep(times[1], 200);
|
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addAddVertexSteps(500, 150);
|
addAddVertexSteps(500, 150);
|
||||||
addVertexCheckStep(3, 1, 500, 150);
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
addAddVertexSteps(90, 220);
|
addAddVertexSteps(90, 200);
|
||||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||||
|
|
||||||
addAddVertexSteps(750, 180);
|
addAddVertexSteps(750, 180);
|
||||||
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
for (int i = 1; i < times.Length; i++)
|
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();
|
var sliderPath = new SliderPath();
|
||||||
path.ConvertToSliderPath(sliderPath, 0);
|
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||||
addBlueprintStep(times[0], positions[0], sliderPath, 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}", () =>
|
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;
|
float expectedX = x - hitObject.OriginalX;
|
||||||
var vertices = getVertices();
|
var vertices = getVertices();
|
||||||
return vertices.Count == count &&
|
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);
|
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
double time = rng.NextDouble() * scale * 2 - scale;
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
distance = Math.Round(distance);
|
time = Math.Round(time);
|
||||||
|
|
||||||
float oldX = path.PositionAtDistance(distance);
|
float oldX = path.PositionAtTime(time);
|
||||||
int index = path.InsertVertex(distance);
|
int index = path.InsertVertex(time);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
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));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
case 1:
|
case 1:
|
||||||
{
|
{
|
||||||
int index = rng.Next(path.Vertices.Count);
|
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);
|
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
||||||
if (integralValues)
|
if (integralValues)
|
||||||
newX = MathF.Round(newX);
|
newX = MathF.Round(newX);
|
||||||
|
|
||||||
path.SetVertexPosition(index, newX);
|
path.SetVertexPosition(index, newX);
|
||||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
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));
|
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertInvariants(path.Vertices, checkSlope);
|
assertInvariants(path.Vertices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Add(10, 5);
|
path.Add(10, 5);
|
||||||
path.Add(20, -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(removeCount, Is.EqualTo(1));
|
||||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||||
{
|
{
|
||||||
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase(10)]
|
||||||
public void TestRandomConvertFromSliderPath()
|
[TestCase(0.1)]
|
||||||
|
public void TestRandomConvertFromSliderPath(double velocity)
|
||||||
{
|
{
|
||||||
var rng = new Random(1);
|
var rng = new Random(1);
|
||||||
var path = new JuiceStreamPath();
|
var path = new JuiceStreamPath();
|
||||||
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
else
|
else
|
||||||
sliderPath.ExpectedDistance.Value = null;
|
sliderPath.ExpectedDistance.Value = null;
|
||||||
|
|
||||||
path.ConvertFromSliderPath(sliderPath);
|
path.ConvertFromSliderPath(sliderPath, velocity);
|
||||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
||||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
|
|
||||||
path.ResampleVertices(sampleDistances);
|
path.ResampleVertices(sampleTimes);
|
||||||
assertInvariants(path.Vertices, true);
|
assertInvariants(path.Vertices);
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * 1e3;
|
double time = rng.NextDouble() * 1e3;
|
||||||
float x = (float)(rng.NextDouble() * 1e3);
|
float x = (float)(rng.NextDouble() * 1e3);
|
||||||
path.Add(distance, x);
|
path.Add(time, x);
|
||||||
} while (rng.Next(5) != 0);
|
} while (rng.Next(5) != 0);
|
||||||
|
|
||||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||||
|
|
||||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
|
||||||
assertInvariants(path.Vertices, true);
|
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||||
|
|
||||||
foreach (var point in sliderPath.ControlPoints)
|
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(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++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
double distance = rng.NextDouble() * path.Distance;
|
double time = rng.NextDouble() * path.Duration;
|
||||||
float expected = path.PositionAtDistance(distance);
|
float expected = path.PositionAtTime(time);
|
||||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
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);
|
path.Add(20, 0);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
path.RemoveVertices((v, _) => v.Time == 20);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||||
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
path.Clear();
|
path.Clear();
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
path.ConvertFromSliderPath(new SliderPath());
|
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||||
checkNewId();
|
checkNewId();
|
||||||
|
|
||||||
void 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);
|
Assert.That(vertices, Is.Not.Empty);
|
||||||
|
|
||||||
for (int i = 0; i < vertices.Count; i++)
|
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));
|
Assert.That(float.IsFinite(vertices[i].X));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 1; i < vertices.Count; i++)
|
for (int i = 1; i < vertices.Count; i++)
|
||||||
{
|
{
|
||||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public int VertexCount => path.Vertices.Count;
|
public int VertexCount => path.Vertices.Count;
|
||||||
|
|
||||||
protected readonly Func<float, double> PositionToDistance;
|
protected readonly Func<float, double> PositionToTime;
|
||||||
|
|
||||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||||
|
|
||||||
protected EditablePath(Func<float, double> positionToDistance)
|
protected EditablePath(Func<float, double> positionToTime)
|
||||||
{
|
{
|
||||||
PositionToDistance = positionToDistance;
|
PositionToTime = positionToTime;
|
||||||
|
|
||||||
Anchor = Anchor.BottomLeft;
|
Anchor = Anchor.BottomLeft;
|
||||||
}
|
}
|
||||||
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
while (InternalChildren.Count < path.Vertices.Count)
|
while (InternalChildren.Count < path.Vertices.Count)
|
||||||
AddInternal(new VertexPiece());
|
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++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
var piece = (VertexPiece)InternalChildren[i];
|
var piece = (VertexPiece)InternalChildren[i];
|
||||||
var vertex = path.Vertices[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]);
|
piece.UpdateFrom(vertexStates[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
var sliderPath = hitObject.Path;
|
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 the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||||
{
|
{
|
||||||
path.ResampleVertices(hitObject.NestedHitObjects
|
path.ResampleVertices(hitObject.NestedHitObjects
|
||||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
.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();
|
vertexStates.Clear();
|
||||||
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
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;
|
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);
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
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 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);
|
path.SetVertexPosition(index, x);
|
||||||
vertexStates.Insert(index, new VertexState());
|
vertexStates.Insert(index, new VertexState());
|
||||||
|
|
||||||
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
return true;
|
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.Clear();
|
||||||
previousVertexStates.AddRange(vertexStates);
|
previousVertexStates.AddRange(vertexStates);
|
||||||
|
|
||||||
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
for (int i = 1; i < vertexCount; i++)
|
for (int i = 1; i < vertexCount; i++)
|
||||||
{
|
{
|
||||||
var state = previousVertexStates[i];
|
var state = previousVertexStates[i];
|
||||||
double distance = state.VertexBeforeChange.Distance;
|
double time = state.VertexBeforeChange.Time;
|
||||||
if (state.IsSelected)
|
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);
|
vertexStates.Insert(newIndex, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private JuiceStreamPathVertex lastVertex;
|
private JuiceStreamPathVertex lastVertex;
|
||||||
|
|
||||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddNewVertex()
|
public void AddNewVertex()
|
||||||
{
|
{
|
||||||
var endVertex = Vertices[^1];
|
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++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
{
|
{
|
||||||
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
Vector2 position = ToRelativePosition(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;
|
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 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()
|
public ScrollingPath()
|
||||||
{
|
{
|
||||||
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
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
|
drawablePath.Vertices = vertices
|
||||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeDistanceXs(JuiceStream hitObject)
|
private void computeTimeXs(JuiceStream hitObject)
|
||||||
{
|
{
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
|
|
||||||
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
if (sliderVertices.Count == 0)
|
if (sliderVertices.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
Vector2 lastPosition = Vector2.Zero;
|
Vector2 lastPosition = Vector2.Zero;
|
||||||
|
|
||||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||||
{
|
{
|
||||||
foreach (var position in sliderVertices)
|
foreach (var position in sliderVertices)
|
||||||
{
|
{
|
||||||
distance += Vector2.Distance(lastPosition, position);
|
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||||
lastPosition = position;
|
lastPosition = position;
|
||||||
|
|
||||||
vertices.Add((distance, position.X));
|
vertices.Add((time, position.X));
|
||||||
}
|
}
|
||||||
|
|
||||||
sliderVertices.Reverse();
|
sliderVertices.Reverse();
|
||||||
|
@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||||
: base(positionToDistance)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddVertex(Vector2 relativePosition)
|
public void AddVertex(Vector2 relativePosition)
|
||||||
{
|
{
|
||||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||||
int index = AddVertex(distance, relativePosition.X);
|
int index = AddVertex(time, relativePosition.X);
|
||||||
selectOnly(index);
|
selectOnly(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
protected override void OnDrag(DragEvent e)
|
protected override void OnDrag(DragEvent e)
|
||||||
{
|
{
|
||||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
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;
|
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||||
MoveSelectedVertices(distanceDelta, xDelta);
|
MoveSelectedVertices(timeDelta, xDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
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;
|
lastEditablePathId = editablePath.PathId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToDistance(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
{
|
{
|
||||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
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(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
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);
|
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);
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
return time - HitObject.StartTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeJuiceStreamPath()
|
private void initializeJuiceStreamPath()
|
||||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input;
|
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 })
|
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;
|
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||||
|
|
||||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
|
||||||
{
|
{
|
||||||
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;
|
return result;
|
||||||
|
@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int RepeatCount { get; set; }
|
public int RepeatCount { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double Velocity { get; private set; }
|
private double velocityFactor;
|
||||||
|
|
||||||
[JsonIgnore]
|
[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>
|
/// <summary>
|
||||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
/// 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);
|
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||||
|
|
||||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||||
|
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
|
||||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
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.
|
/// 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.
|
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
||||||
/// </para>
|
/// </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>
|
/// </summary>
|
||||||
public class JuiceStreamPath
|
public class JuiceStreamPath
|
||||||
{
|
{
|
||||||
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public int InvalidationID { get; private set; } = 1;
|
public int InvalidationID { get; private set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This list should always be non-empty.
|
/// This list should always be non-empty.
|
||||||
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
public float PositionAtDistance(double distance)
|
public float PositionAtTime(double time)
|
||||||
{
|
{
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
return positionAtDistance(distance, index);
|
return positionAtTime(time, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Insert a vertex at given <paramref name="distance"/>.
|
/// Insert a vertex at given <paramref name="time"/>.
|
||||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
/// 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).
|
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The index of the new vertex.</returns>
|
/// <returns>The index of the new vertex.</returns>
|
||||||
public int InsertVertex(double distance)
|
public int InsertVertex(double time)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
throw new ArgumentOutOfRangeException(nameof(time));
|
||||||
|
|
||||||
int index = vertexIndexAtDistance(distance);
|
int index = vertexIndexAtTime(time);
|
||||||
float x = positionAtDistance(distance, index);
|
float x = positionAtTime(time, index);
|
||||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
return index;
|
return index;
|
||||||
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
/// 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>
|
/// </summary>
|
||||||
public void SetVertexPosition(int index, float newX)
|
public void SetVertexPosition(int index, float newX)
|
||||||
{
|
{
|
||||||
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
if (!float.IsFinite(newX))
|
if (!float.IsFinite(newX))
|
||||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||||
|
|
||||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, 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;
|
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
|
||||||
/// </summary>
|
/// </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);
|
SetVertexPosition(index, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recreate this path by using difference set of vertices at given distances.
|
/// Recreate this path by using difference set of vertices at given time points.
|
||||||
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
|
/// 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="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
|
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||||
{
|
{
|
||||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||||
|
|
||||||
foreach (double distance in sampleDistances)
|
foreach (double time in sampleTimes)
|
||||||
{
|
{
|
||||||
if (!double.IsFinite(distance))
|
if (!double.IsFinite(time))
|
||||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||||
|
|
||||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||||
float x = PositionAtDistance(clampedDistance);
|
float x = PositionAtTime(clampedTime);
|
||||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
sampledVertices.Sort();
|
sampledVertices.Sort();
|
||||||
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Duplicated vertices are automatically removed.
|
/// Duplicated vertices are automatically removed.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||||
{
|
{
|
||||||
var sliderPathVertices = new List<Vector2>();
|
var sliderPathVertices = new List<Vector2>();
|
||||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||||
|
|
||||||
double distance = 0;
|
double time = 0;
|
||||||
|
|
||||||
vertices.Clear();
|
vertices.Clear();
|
||||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||||
|
|
||||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
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))
|
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
Add(time, sliderPathVertices[i].X);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate();
|
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>
|
/// <summary>
|
||||||
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
/// 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 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>
|
/// </summary>
|
||||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||||
{
|
{
|
||||||
const float margin = 1;
|
const float margin = 1;
|
||||||
|
|
||||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
// 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);
|
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||||
|
|
||||||
sliderPath.ControlPoints.Clear();
|
sliderPath.ControlPoints.Clear();
|
||||||
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||||
|
|
||||||
float deltaX = vertices[i].X - lastPosition.X;
|
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`.
|
// 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));
|
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||||
|
|
||||||
// When `deltaY` is small, one segment is always enough.
|
// 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);
|
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||||
lastPosition = nextPosition;
|
lastPosition = nextPosition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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.
|
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||||
return i < 0 ? ~i : i;
|
return i < 0 ? ~i : i;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private float positionAtDistance(double distance, int index)
|
private float positionAtTime(double time, int index)
|
||||||
{
|
{
|
||||||
if (index <= 0)
|
if (index <= 0)
|
||||||
return vertices[0].X;
|
return vertices[0].X;
|
||||||
if (index >= vertices.Count)
|
if (index >= vertices.Count)
|
||||||
return vertices[^1].X;
|
return vertices[^1].X;
|
||||||
|
|
||||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||||
if (Precision.AlmostEquals(length, 0))
|
if (Precision.AlmostEquals(duration, 0))
|
||||||
return vertices[index].X;
|
return vertices[index].X;
|
||||||
|
|
||||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||||
|
|
||||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||||
}
|
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidate() => InvalidationID++;
|
private void invalidate() => InvalidationID++;
|
||||||
|
@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||||
{
|
{
|
||||||
public readonly double Distance;
|
public readonly double Time;
|
||||||
|
|
||||||
public readonly float X;
|
public readonly float X;
|
||||||
|
|
||||||
public JuiceStreamPathVertex(double distance, float x)
|
public JuiceStreamPathVertex(double time, float x)
|
||||||
{
|
{
|
||||||
Distance = distance;
|
Time = time;
|
||||||
X = x;
|
X = x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CompareTo(JuiceStreamPathVertex other)
|
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);
|
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;
|
set => InternalChild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
@ -45,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
{
|
{
|
||||||
(typeof(EditorBeatmap), editorBeatmap),
|
(typeof(EditorBeatmap), editorBeatmap),
|
||||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||||
},
|
},
|
||||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
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";
|
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)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, 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)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
foreach (var v in base.ToDatabaseAttributes())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
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_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
||||||
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values);
|
base.FromDatabaseAttributes(values);
|
||||||
|
|
||||||
|
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
|
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.
|
// 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),
|
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||||
ScoreMultiplier = getScoreMultiplier(mods),
|
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)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
var sortedObjects = beatmap.HitObjects.ToArray();
|
var sortedObjects = beatmap.HitObjects.ToArray();
|
||||||
|
@ -5,7 +5,10 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||||
@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
base.UpdateTimeAndPosition(result);
|
base.UpdateTimeAndPosition(result);
|
||||||
|
|
||||||
if (PlacementActive == PlacementState.Waiting)
|
if (result.Playfield is Column col)
|
||||||
Column = result.Playfield as Column;
|
{
|
||||||
|
// 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.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Input;
|
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.Mania.UI;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||||
Playfield.GetColumnByPosition(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)
|
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||||
{
|
{
|
||||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
MuteComboCount = { Value = 0 },
|
MuteComboCount = { Value = 0 },
|
||||||
},
|
},
|
||||||
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.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>
|
/// <summary>
|
||||||
|
@ -17,11 +17,11 @@ using osu.Framework.Testing.Input;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public TestSceneGameplayCursor()
|
public TestSceneGameplayCursor()
|
||||||
{
|
{
|
||||||
var ruleset = new OsuRuleset();
|
var ruleset = new OsuRuleset();
|
||||||
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
|
gameplayState = TestGameplayState.Create(ruleset);
|
||||||
|
|
||||||
AddStep("change background colour", () =>
|
AddStep("change background colour", () =>
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
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 snapResult;
|
||||||
|
|
||||||
return new SnapResult(screenSpacePosition, null);
|
if (snapType.HasFlagFast(SnapType.Grids))
|
||||||
}
|
|
||||||
|
|
||||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
|
||||||
{
|
|
||||||
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
|
||||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
|
||||||
return positionSnap;
|
|
||||||
|
|
||||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
|
||||||
{
|
{
|
||||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
{
|
||||||
|
(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)
|
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||||
{
|
|
||||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
|
||||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
if (positionInfo == positionInfos.First())
|
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);
|
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
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
|
#endregion
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
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; }
|
private DrawableHitObject? drawableObject { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[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
|
AddInternal(ApproachCircle = new Sprite
|
||||||
{
|
{
|
||||||
|
@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (!(osuObject is Slider slider))
|
if (!(osuObject is Slider slider))
|
||||||
return;
|
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<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));
|
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))
|
if (!(osuObject is Slider slider))
|
||||||
return;
|
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<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));
|
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);
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
foreach (OsuHitObject hitObject in hitObjects)
|
foreach (OsuHitObject hitObject in hitObjects)
|
||||||
{
|
{
|
||||||
Vector2 relativePosition = hitObject.Position - previousPosition;
|
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;
|
float relativeAngle = absoluteAngle - previousAngle;
|
||||||
|
|
||||||
positionInfos.Add(new ObjectPositionInfo(hitObject)
|
ObjectPositionInfo positionInfo;
|
||||||
|
positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
|
||||||
{
|
{
|
||||||
RelativeAngle = relativeAngle,
|
RelativeAngle = relativeAngle,
|
||||||
DistanceFromPrevious = relativePosition.Length
|
DistanceFromPrevious = relativePosition.Length
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hitObject is Slider slider)
|
||||||
|
{
|
||||||
|
float absoluteRotation = getSliderRotation(slider);
|
||||||
|
positionInfo.Rotation = absoluteRotation - absoluteAngle;
|
||||||
|
absoluteAngle = absoluteRotation;
|
||||||
|
}
|
||||||
|
|
||||||
previousPosition = hitObject.EndPosition;
|
previousPosition = hitObject.EndPosition;
|
||||||
previousAngle = absoluteAngle;
|
previousAngle = absoluteAngle;
|
||||||
}
|
}
|
||||||
@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
if (hitObject is Spinner)
|
if (hitObject is Spinner)
|
||||||
{
|
{
|
||||||
previous = null;
|
previous = current;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
if (previous != null)
|
if (previous != null)
|
||||||
{
|
{
|
||||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
if (previous.HitObject is Slider s)
|
||||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
{
|
||||||
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
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;
|
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
|
||||||
|
|
||||||
var posRelativeToPrev = new Vector2(
|
var posRelativeToPrev = new Vector2(
|
||||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
|
current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
|
||||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
|
current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
|
||||||
);
|
);
|
||||||
|
|
||||||
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
||||||
@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
||||||
|
|
||||||
current.PositionModified = 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>
|
/// <summary>
|
||||||
@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
var previousPosition = workingObject.PositionModified;
|
var previousPosition = workingObject.PositionModified;
|
||||||
|
|
||||||
// Clamp slider position to the placement area
|
// 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
|
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);
|
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
||||||
|
|
||||||
float newY = possibleMovementBounds.Height < 0
|
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);
|
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
||||||
|
|
||||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
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
|
public class ObjectPositionInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public float DistanceFromPrevious { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
|
[TestCase(1.9971301024093662d, 200, "diffcalc-test")]
|
||||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
|
[TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
|
||||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||||
|
|
||||||
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
|
[TestCase(3.1645810961313674d, 200, "diffcalc-test")]
|
||||||
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
|
[TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
|
||||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
=> 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.
|
// 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.
|
// 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.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
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 SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.4;
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
|
||||||
/// <summary>
|
private readonly SingleKeyStamina[] centreKeyStamina =
|
||||||
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
|
{
|
||||||
/// </summary>
|
new SingleKeyStamina(),
|
||||||
private const int max_history_length = 2;
|
new SingleKeyStamina()
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly SingleKeyStamina[] rimKeyStamina =
|
||||||
|
{
|
||||||
|
new SingleKeyStamina(),
|
||||||
|
new SingleKeyStamina()
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
private int centreKeyIndex;
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
|
private int rimKeyIndex;
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="Stamina"/> skill.
|
/// Creates a <see cref="Stamina"/> skill.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
/// <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)
|
||||||
public Stamina(Mod[] mods, bool rightHand)
|
|
||||||
: base(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)
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
@ -65,52 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
}
|
}
|
||||||
|
|
||||||
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||||
|
return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
private const double rhythm_skill_multiplier = 0.014;
|
private const double rhythm_skill_multiplier = 0.014;
|
||||||
private const double colour_skill_multiplier = 0.01;
|
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)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
@ -33,8 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
new Colour(mods),
|
new Colour(mods),
|
||||||
new Rhythm(mods),
|
new Rhythm(mods),
|
||||||
new Stamina(mods, true),
|
new Stamina(mods)
|
||||||
new Stamina(mods, false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
@ -58,7 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
|
|
||||||
return taikoDifficultyHitObjects;
|
return taikoDifficultyHitObjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,17 +67,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
var colour = (Colour)skills[0];
|
var colour = (Colour)skills[0];
|
||||||
var rhythm = (Rhythm)skills[1];
|
var rhythm = (Rhythm)skills[1];
|
||||||
var staminaRight = (Stamina)skills[2];
|
var stamina = (Stamina)skills[2];
|
||||||
var staminaLeft = (Stamina)skills[3];
|
|
||||||
|
|
||||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_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);
|
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
|
||||||
staminaRating *= staminaPenalty;
|
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 separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
|
||||||
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
||||||
starRating = rescale(starRating);
|
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.
|
/// 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).
|
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||||
/// </remarks>
|
/// </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>();
|
List<double> peaks = new List<double>();
|
||||||
|
|
||||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||||
var staminaRightPeaks = staminaRight.GetCurrentStrainPeaks().ToList();
|
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||||
var staminaLeftPeaks = staminaLeft.GetCurrentStrainPeaks().ToList();
|
|
||||||
|
|
||||||
for (int i = 0; i < colourPeaks.Count; i++)
|
for (int i = 0; i < colourPeaks.Count; i++)
|
||||||
{
|
{
|
||||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||||
double rhythmPeak = rhythmPeaks[i] * rhythm_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);
|
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
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);
|
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||||
difficultyValue *= lengthBonus;
|
difficultyValue *= lengthBonus;
|
||||||
|
@ -863,5 +863,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
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()))
|
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||||
originalLength = stream.Length;
|
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);
|
stream.WriteByte(0);
|
||||||
|
|
||||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
realm.Realm.Write(() =>
|
await realm.Realm.WriteAsync(() =>
|
||||||
{
|
{
|
||||||
foreach (var b in imported.Beatmaps)
|
foreach (var b in imported.Beatmaps)
|
||||||
b.OnlineID = -1;
|
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 });
|
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.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||||
|
|
||||||
// No header shouldn't cause any change
|
// 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.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||||
|
|
||||||
// Reset with a miss instead.
|
// 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)
|
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.TotalScore.Value, Is.Zero);
|
||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||||
|
|
||||||
// Reset with no judged hit.
|
// 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)
|
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||||
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
|
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
|
||||||
|
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestJudgement : Judgement
|
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.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -23,7 +25,10 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private BindableBeatDivisor bindableBeatDivisor;
|
private BindableBeatDivisor bindableBeatDivisor;
|
||||||
|
|
||||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
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]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
@ -47,6 +48,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
(typeof(EditorBeatmap), editorBeatmap),
|
(typeof(EditorBeatmap), editorBeatmap),
|
||||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||||
},
|
},
|
||||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||||
};
|
};
|
||||||
|
@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private class SnapProvider : IDistanceSnapProvider
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
{
|
{
|
||||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||||
new SnapResult(screenSpacePosition, null);
|
|
||||||
|
|
||||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
|
||||||
|
|
||||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||||
|
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
@ -13,6 +16,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneEditorClock : EditorClockTestScene
|
public class TestSceneEditorClock : EditorClockTestScene
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
|
||||||
|
|
||||||
public TestSceneEditorClock()
|
public TestSceneEditorClock()
|
||||||
{
|
{
|
||||||
Add(new FillFlowContainer
|
Add(new FillFlowContainer
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit.Components.Menus;
|
using osu.Game.Screens.Edit.Components.Menus;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneEditorMenuBar : OsuTestScene
|
public class TestSceneEditorMenuBar : OsuTestScene
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||||
|
|
||||||
public TestSceneEditorMenuBar()
|
public TestSceneEditorMenuBar()
|
||||||
{
|
{
|
||||||
Add(new Container
|
Add(new Container
|
||||||
|
@ -12,7 +12,7 @@ using osuTK;
|
|||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestScenePlaybackControl : OsuTestScene
|
public class TestScenePlaybackControl : EditorClockTestScene
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
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 System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
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);
|
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]
|
[Test]
|
||||||
public void TestDisallowZeroDurationObjects()
|
public void TestDisallowZeroDurationObjects()
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -14,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
|
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
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.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||||
|
|
||||||
|
private TimingScreen timingScreen;
|
||||||
|
|
||||||
protected override bool ScrollUsingMouseWheel => false;
|
protected override bool ScrollUsingMouseWheel => false;
|
||||||
|
|
||||||
public TestSceneTimingScreen()
|
public TestSceneTimingScreen()
|
||||||
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||||
Beatmap.Disabled = true;
|
Beatmap.Disabled = true;
|
||||||
|
|
||||||
Child = new TimingScreen
|
Child = timingScreen = new TimingScreen
|
||||||
{
|
{
|
||||||
State = { Value = Visibility.Visible },
|
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)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
Beatmap.Disabled = false;
|
Beatmap.Disabled = false;
|
||||||
|
@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = OsuColour.Gray(30)
|
Colour = OsuColour.Gray(30)
|
||||||
},
|
},
|
||||||
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
|
scrollContainer = new ZoomableScrollContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new MenuCursor()
|
new MenuCursor()
|
||||||
@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestWidthInitialization()
|
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]
|
[Test]
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Screens.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);
|
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
Add(new OsuContextMenuContainer
|
||||||
{
|
{
|
||||||
EditorBeatmap,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Composer,
|
Children = new Drawable[]
|
||||||
new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
EditorBeatmap,
|
||||||
Direction = FillDirection.Vertical,
|
Composer,
|
||||||
Spacing = new Vector2(0, 5),
|
new FillFlowContainer
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new StartStopButton(),
|
AutoSizeAxes = Axes.Both,
|
||||||
new AudioVisualiser(),
|
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 NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
@ -22,7 +21,6 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -33,18 +31,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SkinManager skinManager { get; set; }
|
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;
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -81,11 +67,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
if (expectedComponentsContainer == null)
|
if (expectedComponentsContainer == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var expectedComponentsAdjustmentContainer = new Container
|
var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer
|
||||||
{
|
{
|
||||||
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
|
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
|
||||||
Size = actualComponentsContainer.DrawSize,
|
Size = actualComponentsContainer.DrawSize,
|
||||||
Child = expectedComponentsContainer,
|
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);
|
Add(expectedComponentsAdjustmentContainer);
|
||||||
|
@ -15,7 +15,7 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
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.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -14,16 +13,15 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Input.StateChanges;
|
using osu.Framework.Input.StateChanges;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Mods;
|
using osu.Game.Tests.Mods;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -41,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private TestReplayRecorder recorder;
|
private TestReplayRecorder recorder;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
|
@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning.Editor;
|
using osu.Game.Skinning.Editor;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
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.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
@ -18,8 +18,8 @@ using osu.Game.Rulesets.UI;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Visual.Multiplayer;
|
using osu.Game.Tests.Visual.Multiplayer;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -259,12 +259,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestFinalFramesPurgedBeforeEndingPlay()
|
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", () =>
|
AddStep("send frames and finish play", () =>
|
||||||
{
|
{
|
||||||
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
|
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.
|
// 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;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Mods;
|
using osu.Game.Tests.Mods;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
CachedDependencies = new[]
|
CachedDependencies = new[]
|
||||||
{
|
{
|
||||||
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
|
(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[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -124,13 +124,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
Status = { Value = new RoomStatusOpen() },
|
Status = { Value = new RoomStatusOpen() },
|
||||||
Category = { Value = RoomCategory.Spotlight },
|
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("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]
|
[Test]
|
||||||
|
@ -9,7 +9,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -73,19 +72,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
createFreeModSelect();
|
createFreeModSelect();
|
||||||
|
|
||||||
|
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||||
|
|
||||||
AddStep("click select all button", () =>
|
AddStep("click select all button", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().ElementAt(1));
|
InputManager.MoveMouseTo(this.ChildrenOfType<SelectAllModsButton>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||||
|
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||||
|
|
||||||
AddStep("click deselect all button", () =>
|
AddStep("click deselect all button", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
|
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||||
|
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createFreeModSelect()
|
private void createFreeModSelect()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -15,13 +16,14 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
using osu.Game.Online.Rooms;
|
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Screens.Play.PlayerSettings;
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
@ -349,15 +351,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests spectating with a gameplay start time set to a negative value.
|
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value.
|
||||||
/// Simulating beatmaps with high <see cref="BeatmapInfo.AudioLeadIn"/> or negative time storyboard elements.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[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);
|
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().
|
// 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)
|
// (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);
|
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);
|
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
|
||||||
Ruleset.Value = importedBeatmap.Ruleset;
|
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));
|
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 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 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.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
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.Screens.OnlinePlay.Multiplayer.Participants;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
@ -178,6 +180,47 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
|
.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]
|
[Test]
|
||||||
public void TestNextPlaylistItemSelectedAfterCompletion()
|
public void TestNextPlaylistItemSelectedAfterCompletion()
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
if (isDisposing)
|
if (isDisposing)
|
||||||
return;
|
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))
|
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
|
||||||
stream.CopyTo(outStream);
|
stream.CopyTo(outStream);
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,11 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Collections;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.Leaderboards;
|
using osu.Game.Online.Leaderboards;
|
||||||
@ -54,6 +56,39 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
exitViaEscapeAndConfirm();
|
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>
|
/// <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
|
/// 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).
|
/// 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("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
|
||||||
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
|
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
|
||||||
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
|
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]
|
[Test]
|
||||||
|
@ -10,7 +10,10 @@ using osu.Game.Rulesets;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps.Drawables;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||||
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
|
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -101,6 +104,14 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet()));
|
AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet()));
|
||||||
downloadAssert(true);
|
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]
|
[Test]
|
||||||
@ -232,6 +243,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
|
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
|
||||||
Retries = Enumerable.Range(-2, 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.Online.Chat;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Chat.ChannelList;
|
using osu.Game.Overlays.Chat.ChannelList;
|
||||||
|
using osu.Game.Overlays.Chat.Listing;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
||||||
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
||||||
selected.Value = null;
|
selected.Value = channelList.ChannelListingChannel;
|
||||||
channelList.RemoveChannel(channel);
|
channelList.RemoveChannel(channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,6 +112,12 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
channelList.AddChannel(createRandomPrivateChannel());
|
channelList.AddChannel(createRandomPrivateChannel());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddStep("Add Announce Channels", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
channelList.AddChannel(createRandomAnnounceChannel());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -118,35 +125,37 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("Unread Selected", () =>
|
AddStep("Unread Selected", () =>
|
||||||
{
|
{
|
||||||
if (selected.Value != null)
|
if (validItem)
|
||||||
channelList.GetItem(selected.Value).Unread.Value = true;
|
channelList.GetItem(selected.Value).Unread.Value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Read Selected", () =>
|
AddStep("Read Selected", () =>
|
||||||
{
|
{
|
||||||
if (selected.Value != null)
|
if (validItem)
|
||||||
channelList.GetItem(selected.Value).Unread.Value = false;
|
channelList.GetItem(selected.Value).Unread.Value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Add Mention Selected", () =>
|
AddStep("Add Mention Selected", () =>
|
||||||
{
|
{
|
||||||
if (selected.Value != null)
|
if (validItem)
|
||||||
channelList.GetItem(selected.Value).Mentions.Value++;
|
channelList.GetItem(selected.Value).Mentions.Value++;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Add 98 Mentions Selected", () =>
|
AddStep("Add 98 Mentions Selected", () =>
|
||||||
{
|
{
|
||||||
if (selected.Value != null)
|
if (validItem)
|
||||||
channelList.GetItem(selected.Value).Mentions.Value += 98;
|
channelList.GetItem(selected.Value).Mentions.Value += 98;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Clear Mentions Selected", () =>
|
AddStep("Clear Mentions Selected", () =>
|
||||||
{
|
{
|
||||||
if (selected.Value != null)
|
if (validItem)
|
||||||
channelList.GetItem(selected.Value).Mentions.Value = 0;
|
channelList.GetItem(selected.Value).Mentions.Value = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool validItem => selected.Value != null && !(selected.Value is ChannelListing.ChannelListingChannel);
|
||||||
|
|
||||||
private Channel createRandomPublicChannel()
|
private Channel createRandomPublicChannel()
|
||||||
{
|
{
|
||||||
int id = RNG.Next(0, 10000);
|
int id = RNG.Next(0, 10000);
|
||||||
@ -167,5 +176,16 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Username = $"test user {id}",
|
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.Graphics;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Overlays.Chat;
|
using osu.Game.Overlays.Chat;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -22,12 +21,10 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
public class TestSceneChatLink : OsuTestScene
|
public class TestSceneChatLink : OsuTestScene
|
||||||
{
|
{
|
||||||
private readonly TestChatLineContainer textContainer;
|
private readonly TestChatLineContainer textContainer;
|
||||||
private readonly DialogOverlay dialogOverlay;
|
|
||||||
private Color4 linkColour;
|
private Color4 linkColour;
|
||||||
|
|
||||||
public TestSceneChatLink()
|
public TestSceneChatLink()
|
||||||
{
|
{
|
||||||
Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue });
|
|
||||||
Add(textContainer = new TestChatLineContainer
|
Add(textContainer = new TestChatLineContainer
|
||||||
{
|
{
|
||||||
Padding = new MarginPadding { Left = 20, Right = 20 },
|
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 = "#english" });
|
||||||
availableChannels.Add(new Channel { Name = "#japanese" });
|
availableChannels.Add(new Channel { Name = "#japanese" });
|
||||||
Dependencies.Cache(chatManager);
|
Dependencies.Cache(chatManager);
|
||||||
|
|
||||||
Dependencies.Cache(new ChatOverlay());
|
|
||||||
Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[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.Database;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Dashboard;
|
using osu.Game.Overlays.Dashboard;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
CachedDependencies = new (Type, object)[]
|
CachedDependencies = new (Type, object)[]
|
||||||
{
|
{
|
||||||
(typeof(SpectatorClient), spectatorClient),
|
(typeof(SpectatorClient), spectatorClient),
|
||||||
(typeof(UserLookupCache), lookupCache)
|
(typeof(UserLookupCache), lookupCache),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
|
||||||
},
|
},
|
||||||
Child = currentlyPlaying = new CurrentlyPlayingDisplay
|
Child = currentlyPlaying = new CurrentlyPlayingDisplay
|
||||||
{
|
{
|
||||||
|
@ -128,11 +128,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
AddAssert("Ensure no adjacent day separators", () =>
|
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)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
AddUntilStep("wait for scores loaded", () =>
|
AddUntilStep("wait for scores loaded", () =>
|
||||||
requestComplete
|
requestComplete
|
||||||
|
// request handler may need to fire more than once to get scores.
|
||||||
|
&& totalCount > 0
|
||||||
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
||||||
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
||||||
AddWaitStep("wait for display", 5);
|
AddWaitStep("wait for display", 5);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -13,6 +14,7 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -114,10 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap);
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap 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
|
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
|
||||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddStep("clear label", () => textBox.LabelText = default);
|
AddStep("clear label", () => textBox.LabelText = default);
|
||||||
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
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));
|
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;
|
SettingsNumberBox numberBox = null;
|
||||||
|
|
||||||
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
|
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!");
|
AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
|
||||||
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
|
||||||
|
|
||||||
AddStep("unset warning text", () => numberBox.WarningText = default);
|
AddStep("unset warning text", () => numberBox.ClearNoticeText());
|
||||||
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
|
AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
|
||||||
|
|
||||||
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
|
AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
|
||||||
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
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<ISettingsItem>()
|
||||||
.OfType<IFilterable>()
|
.OfType<IFilterable>()
|
||||||
.Where(f => !(f is IHasFilterableChildren))
|
.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);
|
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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
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);
|
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNullBeatmap()
|
public void TestNullBeatmap()
|
||||||
{
|
{
|
||||||
@ -147,24 +152,48 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBPMUpdates()
|
public void TestBPMUpdates()
|
||||||
{
|
{
|
||||||
const float bpm = 120;
|
const double bpm = 120;
|
||||||
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
IBeatmap beatmap = createTestBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 60 * 1000 / bpm });
|
||||||
|
|
||||||
OsuModDoubleTime doubleTime = null;
|
OsuModDoubleTime doubleTime = null;
|
||||||
|
|
||||||
selectBeatmap(beatmap);
|
selectBeatmap(beatmap);
|
||||||
checkDisplayedBPM(bpm);
|
checkDisplayedBPM($"{bpm}");
|
||||||
|
|
||||||
AddStep("select DT", () => SelectedMods.Value = new[] { doubleTime = new OsuModDoubleTime() });
|
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);
|
AddStep("change DT rate", () => doubleTime.SpeedChange.Value = 2);
|
||||||
checkDisplayedBPM(bpm * 2);
|
checkDisplayedBPM($"{bpm * 2}");
|
||||||
|
}
|
||||||
|
|
||||||
void checkDisplayedBPM(float target) =>
|
[TestCase(120, 125, null, "120-125 (mostly 120)")]
|
||||||
AddUntilStep($"displayed bpm is {target}", () => this.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any(
|
[TestCase(120, 120.6, null, "120-121 (mostly 120)")]
|
||||||
label => label.Statistic.Name == "BPM" && label.Statistic.Content == target.ToString(CultureInfo.InvariantCulture)));
|
[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)
|
private void setRuleset(RulesetInfo rulesetInfo)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
@ -82,11 +83,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
if (!allowMistimed)
|
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
|
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()
|
private List<TimingControlPoint> timingPoints => BeatSyncSource.ControlPoints?.TimingPoints.ToList();
|
||||||
{
|
|
||||||
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 TimingControlPoint getNextTimingPoint(TimingControlPoint current)
|
private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
|
||||||
{
|
{
|
||||||
@ -292,7 +280,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
if (timingPoints.Count == 0) return 0;
|
if (timingPoints.Count == 0) return 0;
|
||||||
|
|
||||||
if (timingPoints[^1] == current)
|
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);
|
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()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
|
Debug.Assert(BeatSyncSource.Clock != null);
|
||||||
|
|
||||||
timeUntilNextBeat.Value = TimeUntilNextBeat;
|
timeUntilNextBeat.Value = TimeUntilNextBeat;
|
||||||
timeSinceLastBeat.Value = TimeSinceLastBeat;
|
timeSinceLastBeat.Value = TimeSinceLastBeat;
|
||||||
currentTime.Value = BeatSyncClock.CurrentTime;
|
currentTime.Value = BeatSyncSource.Clock.CurrentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Action<int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes> NewBeat;
|
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());
|
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());
|
AddStep("hide", () => overlay.Hide());
|
||||||
AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden);
|
AddAssert("overlay hidden", () => overlay.State.Value == Visibility.Hidden);
|
||||||
@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddStep("run notification action", () => lastNotification.Activated());
|
AddStep("run notification action", () => lastNotification.Activated());
|
||||||
|
|
||||||
AddAssert("overlay shown", () => overlay.State.Value == Visibility.Visible);
|
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.
|
// interface mocks break hot reload, mocking this stub implementation instead works around it.
|
||||||
|
@ -435,15 +435,19 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
createScreen();
|
createScreen();
|
||||||
changeRuleset(0);
|
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() });
|
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("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", () =>
|
AddStep("click deselect all button", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
|
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
|
||||||
|
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("results filtered correctly",
|
AddAssert("results filtered correctly",
|
||||||
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
|
() => playlistOverlay.ChildrenOfType<PlaylistItem>()
|
||||||
.Where(item => item.MatchingFilter)
|
.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 NUnit.Framework;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
@ -15,14 +18,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
private readonly BindableBool enabled = new BindableBool(true);
|
private readonly BindableBool enabled = new BindableBool(true);
|
||||||
|
|
||||||
protected override Drawable CreateContent() => new RoundedButton
|
protected override Drawable CreateContent()
|
||||||
{
|
{
|
||||||
Width = 400,
|
return new FillFlowContainer
|
||||||
Text = "Test button",
|
{
|
||||||
Anchor = Anchor.Centre,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre,
|
Children = new Drawable[]
|
||||||
Enabled = { BindTarget = enabled },
|
{
|
||||||
};
|
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]
|
[Test]
|
||||||
public void TestDisabled()
|
public void TestDisabled()
|
||||||
@ -34,7 +54,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
public void TestBackgroundColour()
|
public void TestBackgroundColour()
|
||||||
{
|
{
|
||||||
AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
|
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);
|
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
|
private string firstAudioFile
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
|
|||||||
// ReSharper disable once AccessToDisposedClosure
|
// ReSharper disable once AccessToDisposedClosure
|
||||||
var storage = host.Storage.GetStorageForDirectory(Path.Combine("tournaments", "default"));
|
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))
|
using (var writer = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
writer.Write(@"{
|
writer.Write(@"{
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Tests.Screens
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(Storage storage)
|
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))
|
using (var writer = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
writer.WriteLine("KR : South Korea : KOR");
|
writer.WriteLine("KR : South Korea : KOR");
|
||||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
|
|
||||||
public void SaveChanges()
|
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))
|
using (var sw = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
sw.Write(JsonConvert.SerializeObject(this,
|
sw.Write(JsonConvert.SerializeObject(this,
|
||||||
|
@ -205,7 +205,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Write to drawings_results
|
// 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))
|
using (StreamWriter sw = new StreamWriter(stream))
|
||||||
{
|
{
|
||||||
sw.Write(text);
|
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)
|
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)
|
if (immediate)
|
||||||
{
|
{
|
||||||
@ -321,7 +321,7 @@ namespace osu.Game.Tournament
|
|||||||
Converters = new JsonConverter[] { new JsonPointConverter() }
|
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))
|
using (var sw = new StreamWriter(stream))
|
||||||
sw.Write(serialisedLadder);
|
sw.Write(serialisedLadder);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps.Timing;
|
using osu.Game.Beatmaps.Timing;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user