mirror of
https://github.com/ppy/osu.git
synced 2026-05-16 19:23:11 +08:00
Compare commits
548 Commits
2022.509.0
...
2022.528.0
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2022.1.0-eap10",
|
||||
"version": "2022.1.1",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
@@ -27,4 +27,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/inspectcode
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
|
||||
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', '.editorconfig', '.globalconfig', 'CodeAnalysis/*') }}
|
||||
|
||||
- name: Dotnet code style
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true
|
||||
@@ -78,15 +78,6 @@ jobs:
|
||||
with:
|
||||
dotnet-version: "6.0.x"
|
||||
|
||||
# FIXME: libavformat is not included in Ubuntu. Let's fix that.
|
||||
# https://github.com/ppy/osu-framework/issues/4349
|
||||
# Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
|
||||
- name: Install libavformat-dev
|
||||
if: ${{matrix.os.fullname == 'ubuntu-latest'}}
|
||||
run: |
|
||||
sudo apt-get update && \
|
||||
sudo apt-get -y install libavformat-dev
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Add Release to Sentry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
sentry_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ppy
|
||||
SENTRY_PROJECT: osu
|
||||
SENTRY_URL: https://sentry.ppy.sh/
|
||||
with:
|
||||
environment: production
|
||||
version: ${{ github.ref }}
|
||||
@@ -340,3 +340,5 @@ inspectcode
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
**/FodyWeavers.xml
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
||||
Generated
-11
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SwUserDefinedSpecifications">
|
||||
<option name="specTypeByUrl">
|
||||
<map />
|
||||
</option>
|
||||
</component>
|
||||
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
|
||||
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -11,6 +11,7 @@ T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal exten
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img width="500px" src="assets/lazer.png">
|
||||
<img width="500" alt="osu! logo" src="assets/lazer.png">
|
||||
</p>
|
||||
|
||||
# osu!
|
||||
@@ -8,6 +8,7 @@
|
||||
[](https://github.com/ppy/osu/releases/latest)
|
||||
[](https://www.codefactor.io/repository/github/ppy/osu)
|
||||
[](https://discord.gg/ppy)
|
||||
[](https://crowdin.com/project/osu-web)
|
||||
|
||||
A free-to-win rhythm game. Rhythm is just a *click* away!
|
||||
|
||||
@@ -31,7 +32,7 @@ If you are looking to install or test osu! without setting up a development envi
|
||||
|
||||
**Latest build:**
|
||||
|
||||
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
|
||||
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) |
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
|
||||
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
|
||||
@@ -104,6 +105,8 @@ When it comes to contributing to the project, the two main things you can do to
|
||||
|
||||
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
|
||||
|
||||
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
||||
|
||||
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
|
||||
|
||||
## Licence
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
+3
-3
@@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.509.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.528.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
<PackageReference Include="Realm" Version="10.10.0" />
|
||||
<PackageReference Include="Realm" Version="10.11.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
@@ -63,8 +61,6 @@ namespace osu.Desktop
|
||||
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
|
||||
{
|
||||
host.ExceptionThrown += handleException;
|
||||
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
|
||||
@@ -131,23 +127,5 @@ namespace osu.Desktop
|
||||
// tools.SetProcessAppUserModelId();
|
||||
});
|
||||
}
|
||||
|
||||
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
|
||||
|
||||
/// <summary>
|
||||
/// Allow a maximum of one unhandled exception, per second of execution.
|
||||
/// </summary>
|
||||
/// <param name="arg"></param>
|
||||
private static bool handleException(Exception arg)
|
||||
{
|
||||
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
||||
|
||||
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
|
||||
|
||||
// restore the stock of allowable exceptions after a short delay.
|
||||
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
|
||||
|
||||
return continueExecution;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.23-gc8da1a" />
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.40" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
<file src="**.exe" target="lib\net45\" exclude="**vshost**"/>
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
</files>
|
||||
</package>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
|
||||
{
|
||||
private const double velocity = 0.5;
|
||||
private const double velocity_factor = 0.5;
|
||||
|
||||
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var playable = base.GetPlayableBeatmap();
|
||||
playable.Difficulty.SliderTickRate = 5;
|
||||
playable.Difficulty.SliderMultiplier = velocity * 10;
|
||||
playable.Difficulty.SliderMultiplier = velocity_factor * 10;
|
||||
return playable;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
|
||||
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
|
||||
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
|
||||
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVelocityLimit()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300 });
|
||||
}
|
||||
addPathCheckStep(times, positions);
|
||||
|
||||
[Test]
|
||||
public void TestPreviousVerticesAreFixed()
|
||||
{
|
||||
double[] times = { 100, 300, 500, 700 };
|
||||
float[] positions = { 200, 400, 100, 500 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
|
||||
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
{
|
||||
double[] times = { 100, 300, 500 };
|
||||
float[] positions = { 200, 200, 0, 250 };
|
||||
float[] positions = { 200, 200, -3000, 250 };
|
||||
|
||||
addMoveAndClickSteps(times[0], positions[0]);
|
||||
addMoveAndClickSteps(times[1], positions[1]);
|
||||
@@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addPathCheckStep(times, new float[] { 200, 200, 250 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFirstVertexIsFixed()
|
||||
{
|
||||
double[] times = { 100, 200 };
|
||||
float[] positions = { 100, 300 };
|
||||
addPlacementSteps(times, positions);
|
||||
addPathCheckStep(times, new float[] { 100, 150 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrder()
|
||||
{
|
||||
|
||||
@@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClampedPositionIsRestored()
|
||||
public void TestSliderVelocityChange()
|
||||
{
|
||||
const double velocity = 0.25;
|
||||
double[] times = { 100, 500, 700 };
|
||||
float[] positions = { 100, 100, 100 };
|
||||
addBlueprintStep(times, positions, velocity);
|
||||
double[] times = { 100, 300 };
|
||||
float[] positions = { 200, 300 };
|
||||
addBlueprintStep(times, positions);
|
||||
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
|
||||
addDragStartStep(times[1], positions[1]);
|
||||
|
||||
AddMouseMoveStep(times[1], 200);
|
||||
addVertexCheckStep(3, 1, times[1], 200);
|
||||
addVertexCheckStep(3, 2, times[2], 150);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
addVertexCheckStep(3, 1, times[1], 100);
|
||||
// Stored position is restored.
|
||||
addVertexCheckStep(3, 2, times[2], positions[2]);
|
||||
|
||||
AddMouseMoveStep(times[1], 300);
|
||||
addDragEndStep();
|
||||
addDragStartStep(times[1], 300);
|
||||
|
||||
AddMouseMoveStep(times[1], 100);
|
||||
// Position is different because a changed position is committed when the previous drag is ended.
|
||||
addVertexCheckStep(3, 2, times[2], 250);
|
||||
AddMouseMoveStep(times[1], 400);
|
||||
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addAddVertexSteps(500, 150);
|
||||
addVertexCheckStep(3, 1, 500, 150);
|
||||
|
||||
addAddVertexSteps(90, 220);
|
||||
addAddVertexSteps(90, 200);
|
||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||
|
||||
addAddVertexSteps(750, 180);
|
||||
@@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
var path = new JuiceStreamPath();
|
||||
for (int i = 1; i < times.Length; i++)
|
||||
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
||||
path.Add(times[i] - times[0], positions[i] - positions[0]);
|
||||
|
||||
var sliderPath = new SliderPath();
|
||||
path.ConvertToSliderPath(sliderPath, 0);
|
||||
path.ConvertToSliderPath(sliderPath, 0, velocity);
|
||||
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||
}
|
||||
|
||||
@@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
||||
{
|
||||
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
|
||||
double expectedTime = time - hitObject.StartTime;
|
||||
float expectedX = x - hitObject.OriginalX;
|
||||
var vertices = getVertices();
|
||||
return vertices.Count == count &&
|
||||
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
|
||||
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
double distance = rng.NextDouble() * scale * 2 - scale;
|
||||
double time = rng.NextDouble() * scale * 2 - scale;
|
||||
if (integralValues)
|
||||
distance = Math.Round(distance);
|
||||
time = Math.Round(time);
|
||||
|
||||
float oldX = path.PositionAtDistance(distance);
|
||||
int index = path.InsertVertex(distance);
|
||||
float oldX = path.PositionAtTime(time);
|
||||
int index = path.InsertVertex(time);
|
||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
|
||||
break;
|
||||
}
|
||||
@@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
case 1:
|
||||
{
|
||||
int index = rng.Next(path.Vertices.Count);
|
||||
double distance = path.Vertices[index].Distance;
|
||||
double time = path.Vertices[index].Time;
|
||||
float newX = (float)(rng.NextDouble() * scale * 2 - scale);
|
||||
if (integralValues)
|
||||
newX = MathF.Round(newX);
|
||||
|
||||
path.SetVertexPosition(index, newX);
|
||||
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
|
||||
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance));
|
||||
Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
|
||||
Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertInvariants(path.Vertices, checkSlope);
|
||||
assertInvariants(path.Vertices);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(10, 5);
|
||||
path.Add(20, -5);
|
||||
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1);
|
||||
int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
|
||||
Assert.That(removeCount, Is.EqualTo(1));
|
||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||
{
|
||||
@@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomConvertFromSliderPath()
|
||||
[TestCase(10)]
|
||||
[TestCase(0.1)]
|
||||
public void TestRandomConvertFromSliderPath(double velocity)
|
||||
{
|
||||
var rng = new Random(1);
|
||||
var path = new JuiceStreamPath();
|
||||
@@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
else
|
||||
sliderPath.ExpectedDistance.Value = null;
|
||||
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0));
|
||||
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||
assertInvariants(path.Vertices, true);
|
||||
path.ConvertFromSliderPath(sliderPath, velocity);
|
||||
Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
|
||||
Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
|
||||
assertInvariants(path.Vertices);
|
||||
|
||||
double[] sampleDistances = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance)
|
||||
.ToArray();
|
||||
double[] sampleTimes = Enumerable.Range(0, 10)
|
||||
.Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
|
||||
.ToArray();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
|
||||
path.ResampleVertices(sampleDistances);
|
||||
assertInvariants(path.Vertices, true);
|
||||
path.ResampleVertices(sampleTimes);
|
||||
assertInvariants(path.Vertices);
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3));
|
||||
float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
|
||||
Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
do
|
||||
{
|
||||
double distance = rng.NextDouble() * 1e3;
|
||||
double time = rng.NextDouble() * 1e3;
|
||||
float x = (float)(rng.NextDouble() * 1e3);
|
||||
path.Add(distance, x);
|
||||
path.Add(time, x);
|
||||
} while (rng.Next(5) != 0);
|
||||
|
||||
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY);
|
||||
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
assertInvariants(path.Vertices, true);
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
double velocity = Math.Clamp(requiredVelocity, 1, 100);
|
||||
|
||||
path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
|
||||
|
||||
foreach (var point in sliderPath.ControlPoints)
|
||||
{
|
||||
@@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
}
|
||||
|
||||
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
|
||||
|
||||
// The path is preserved only if required velocity is used.
|
||||
if (velocity < requiredVelocity) continue;
|
||||
|
||||
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double distance = rng.NextDouble() * path.Distance;
|
||||
float expected = path.PositionAtDistance(distance);
|
||||
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3));
|
||||
double time = rng.NextDouble() * path.Duration;
|
||||
float expected = path.PositionAtTime(time);
|
||||
Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Add(20, 0);
|
||||
checkNewId();
|
||||
|
||||
path.RemoveVertices((v, _) => v.Distance == 20);
|
||||
path.RemoveVertices((v, _) => v.Time == 20);
|
||||
checkNewId();
|
||||
|
||||
path.ResampleVertices(new double[] { 5, 10, 15 });
|
||||
@@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
path.Clear();
|
||||
checkNewId();
|
||||
|
||||
path.ConvertFromSliderPath(new SliderPath());
|
||||
path.ConvertFromSliderPath(new SliderPath(), 1);
|
||||
checkNewId();
|
||||
|
||||
void checkNewId()
|
||||
@@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope)
|
||||
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
|
||||
{
|
||||
Assert.That(vertices, Is.Not.Empty);
|
||||
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(double.IsFinite(vertices[i].Distance));
|
||||
Assert.That(double.IsFinite(vertices[i].Time));
|
||||
Assert.That(float.IsFinite(vertices[i].X));
|
||||
}
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance));
|
||||
|
||||
if (!checkSlope) continue;
|
||||
|
||||
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
|
||||
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
|
||||
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
|
||||
Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public int VertexCount => path.Vertices.Count;
|
||||
|
||||
protected readonly Func<float, double> PositionToDistance;
|
||||
protected readonly Func<float, double> PositionToTime;
|
||||
|
||||
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||
|
||||
@@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||
|
||||
protected EditablePath(Func<float, double> positionToDistance)
|
||||
protected EditablePath(Func<float, double> positionToTime)
|
||||
{
|
||||
PositionToDistance = positionToDistance;
|
||||
PositionToTime = positionToTime;
|
||||
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
@@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
while (InternalChildren.Count < path.Vertices.Count)
|
||||
AddInternal(new VertexPiece());
|
||||
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
var piece = (VertexPiece)InternalChildren[i];
|
||||
var vertex = path.Vertices[i];
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
|
||||
piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
|
||||
piece.UpdateFrom(vertexStates[i]);
|
||||
}
|
||||
}
|
||||
@@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||
{
|
||||
var sliderPath = hitObject.Path;
|
||||
path.ConvertFromSliderPath(sliderPath);
|
||||
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
|
||||
|
||||
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||
{
|
||||
path.ResampleVertices(hitObject.NestedHitObjects
|
||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
|
||||
.Select(h => h.StartTime - hitObject.StartTime));
|
||||
}
|
||||
|
||||
vertexStates.Clear();
|
||||
@@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
|
||||
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
|
||||
double requiredVelocity = path.ComputeRequiredVelocity();
|
||||
|
||||
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
|
||||
// but it should be always rounded up to satisfy the required minimum velocity condition.
|
||||
//
|
||||
// This is rounded to integers instead of using the precision of the bindable
|
||||
// because it results in a smaller number of non-redundant control points.
|
||||
//
|
||||
// The value is clamped here by the bindable min and max values.
|
||||
// In case the required velocity is too large, the path is not preserved.
|
||||
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||
|
||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
||||
|
||||
if (beatSnapProvider == null) return;
|
||||
|
||||
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
||||
double endTime = hitObject.StartTime + path.Duration;
|
||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||
}
|
||||
@@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
protected int AddVertex(double distance, float x)
|
||||
protected int AddVertex(double time, float x)
|
||||
{
|
||||
int index = path.InsertVertex(distance);
|
||||
int index = path.InsertVertex(time);
|
||||
path.SetVertexPosition(index, x);
|
||||
vertexStates.Insert(index, new VertexState());
|
||||
|
||||
@@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
||||
protected void MoveSelectedVertices(double timeDelta, float xDelta)
|
||||
{
|
||||
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
||||
// Because the vertex list may be reordered due to time change, the state list must be reordered as well.
|
||||
previousVertexStates.Clear();
|
||||
previousVertexStates.AddRange(vertexStates);
|
||||
|
||||
@@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
for (int i = 1; i < vertexCount; i++)
|
||||
{
|
||||
var state = previousVertexStates[i];
|
||||
double distance = state.VertexBeforeChange.Distance;
|
||||
double time = state.VertexBeforeChange.Time;
|
||||
if (state.IsSelected)
|
||||
distance += distanceDelta;
|
||||
time += timeDelta;
|
||||
|
||||
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
||||
int newIndex = path.InsertVertex(Math.Max(0, time));
|
||||
vertexStates.Insert(newIndex, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
/// </summary>
|
||||
private JuiceStreamPathVertex lastVertex;
|
||||
|
||||
public PlacementEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public PlacementEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddNewVertex()
|
||||
{
|
||||
var endVertex = Vertices[^1];
|
||||
int index = AddVertex(endVertex.Distance, endVertex.X);
|
||||
int index = AddVertex(endVertex.Time, endVertex.X);
|
||||
|
||||
for (int i = 0; i < VertexCount; i++)
|
||||
{
|
||||
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
public void MoveLastVertex(Vector2 screenSpacePosition)
|
||||
{
|
||||
Vector2 position = ToRelativePosition(screenSpacePosition);
|
||||
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance;
|
||||
double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
|
||||
float xDelta = position.X - lastVertex.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
private readonly Path drawablePath;
|
||||
|
||||
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
|
||||
private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
|
||||
|
||||
public ScrollingPath()
|
||||
{
|
||||
@@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||
{
|
||||
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||
double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
|
||||
|
||||
computeDistanceXs(hitObject);
|
||||
computeTimeXs(hitObject);
|
||||
drawablePath.Vertices = vertices
|
||||
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
|
||||
.Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
|
||||
.ToArray();
|
||||
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
||||
private void computeDistanceXs(JuiceStream hitObject)
|
||||
private void computeTimeXs(JuiceStream hitObject)
|
||||
{
|
||||
vertices.Clear();
|
||||
|
||||
@@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
if (sliderVertices.Count == 0)
|
||||
return;
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
Vector2 lastPosition = Vector2.Zero;
|
||||
|
||||
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
|
||||
{
|
||||
foreach (var position in sliderVertices)
|
||||
{
|
||||
distance += Vector2.Distance(lastPosition, position);
|
||||
time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
|
||||
lastPosition = position;
|
||||
|
||||
vertices.Add((distance, position.X));
|
||||
vertices.Add((time, position.X));
|
||||
}
|
||||
|
||||
sliderVertices.Reverse();
|
||||
|
||||
@@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
[CanBeNull]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
public SelectionEditablePath(Func<float, double> positionToDistance)
|
||||
: base(positionToDistance)
|
||||
public SelectionEditablePath(Func<float, double> positionToTime)
|
||||
: base(positionToTime)
|
||||
{
|
||||
}
|
||||
|
||||
public void AddVertex(Vector2 relativePosition)
|
||||
{
|
||||
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
||||
int index = AddVertex(distance, relativePosition.X);
|
||||
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||
int index = AddVertex(time, relativePosition.X);
|
||||
selectOnly(index);
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
||||
double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
|
||||
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||
MoveSelectedVertices(distanceDelta, xDelta);
|
||||
MoveSelectedVertices(timeDelta, xDelta);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new PlacementEditablePath(positionToDistance)
|
||||
editablePath = new PlacementEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
scrollingPath = new ScrollingPath(),
|
||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||
editablePath = new SelectionEditablePath(positionToDistance)
|
||||
editablePath = new SelectionEditablePath(positionToTime)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||
}
|
||||
|
||||
private double positionToDistance(float relativeYPosition)
|
||||
private double positionToTime(float relativeYPosition)
|
||||
{
|
||||
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||
return time - HitObject.StartTime;
|
||||
}
|
||||
|
||||
private void initializeJuiceStreamPath()
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
@@ -89,15 +90,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
result = snapResult;
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int RepeatCount { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity { get; private set; }
|
||||
private double velocityFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance { get; private set; }
|
||||
private double tickDistanceFactor;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
[JsonIgnore]
|
||||
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
/// <summary>
|
||||
/// The length of one span of this <see cref="JuiceStream"/>.
|
||||
@@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
|
||||
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
TickDistance = scoringDistance / difficulty.SliderTickRate;
|
||||
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
|
||||
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
|
||||
@@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// However, the <see cref="SliderPath"/> representation is difficult to work with.
|
||||
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
|
||||
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
|
||||
/// and this slope condition is always maintained as an invariant.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class JuiceStreamPath
|
||||
{
|
||||
@@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
public int InvalidationID { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>.
|
||||
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
|
||||
/// </summary>
|
||||
public double Distance => vertices[^1].Distance - vertices[0].Distance;
|
||||
public double Duration => vertices[^1].Time - vertices[0].Time;
|
||||
|
||||
/// <remarks>
|
||||
/// This list should always be non-empty.
|
||||
@@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Compute the x-position of the path at the given <paramref name="distance"/>.
|
||||
/// Compute the x-position of the path at the given <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned,
|
||||
/// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
|
||||
/// </remarks>
|
||||
public float PositionAtDistance(double distance)
|
||||
public float PositionAtTime(double time)
|
||||
{
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
return positionAtDistance(distance, index);
|
||||
int index = vertexIndexAtTime(time);
|
||||
return positionAtTime(time, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert a vertex at given <paramref name="distance"/>.
|
||||
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex.
|
||||
/// Insert a vertex at given <paramref name="time"/>.
|
||||
/// The <see cref="PositionAtTime"/> is used as the position of the new vertex.
|
||||
/// Thus, the set of points of the path is not changed (up to floating-point precision).
|
||||
/// </summary>
|
||||
/// <returns>The index of the new vertex.</returns>
|
||||
public int InsertVertex(double distance)
|
||||
public int InsertVertex(double time)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(distance));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(time));
|
||||
|
||||
int index = vertexIndexAtDistance(distance);
|
||||
float x = positionAtDistance(distance, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(distance, x));
|
||||
int index = vertexIndexAtTime(time);
|
||||
float x = positionAtTime(time, index);
|
||||
vertices.Insert(index, new JuiceStreamPathVertex(time, x));
|
||||
|
||||
invalidate();
|
||||
return index;
|
||||
@@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
/// <summary>
|
||||
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
|
||||
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
|
||||
/// </summary>
|
||||
public void SetVertexPosition(int index, float newX)
|
||||
{
|
||||
@@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
if (!float.IsFinite(newX))
|
||||
throw new ArgumentOutOfRangeException(nameof(newX));
|
||||
|
||||
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX);
|
||||
|
||||
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
for (int i = index + 1; i < vertices.Count; i++)
|
||||
{
|
||||
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
|
||||
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
|
||||
}
|
||||
|
||||
vertices[index] = newVertex;
|
||||
vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new vertex at given <paramref name="distance"/> and position.
|
||||
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
|
||||
/// Add a new vertex at given <paramref name="time"/> and position.
|
||||
/// </summary>
|
||||
public void Add(double distance, float x)
|
||||
public void Add(double time, float x)
|
||||
{
|
||||
int index = InsertVertex(distance);
|
||||
int index = InsertVertex(time);
|
||||
SetVertexPosition(index, x);
|
||||
}
|
||||
|
||||
@@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate this path by using difference set of vertices at given distances.
|
||||
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path.
|
||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved.
|
||||
/// Recreate this path by using difference set of vertices at given time points.
|
||||
/// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
|
||||
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
|
||||
/// </summary>
|
||||
public void ResampleVertices(IEnumerable<double> sampleDistances)
|
||||
public void ResampleVertices(IEnumerable<double> sampleTimes)
|
||||
{
|
||||
var sampledVertices = new List<JuiceStreamPathVertex>();
|
||||
|
||||
foreach (double distance in sampleDistances)
|
||||
foreach (double time in sampleTimes)
|
||||
{
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleDistances));
|
||||
if (!double.IsFinite(time))
|
||||
throw new ArgumentOutOfRangeException(nameof(sampleTimes));
|
||||
|
||||
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance);
|
||||
float x = PositionAtDistance(clampedDistance);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x));
|
||||
double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
|
||||
float x = PositionAtTime(clampedTime);
|
||||
sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
|
||||
}
|
||||
|
||||
sampledVertices.Sort();
|
||||
@@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// <remarks>
|
||||
/// Duplicated vertices are automatically removed.
|
||||
/// </remarks>
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath)
|
||||
public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
|
||||
{
|
||||
var sliderPathVertices = new List<Vector2>();
|
||||
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
|
||||
|
||||
double distance = 0;
|
||||
double time = 0;
|
||||
|
||||
vertices.Clear();
|
||||
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
|
||||
|
||||
for (int i = 1; i < sliderPathVertices.Count; i++)
|
||||
{
|
||||
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]);
|
||||
time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
|
||||
|
||||
if (!Precision.AlmostEquals(vertices[^1].Distance, distance))
|
||||
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X));
|
||||
if (!Precision.AlmostEquals(vertices[^1].Time, time))
|
||||
Add(time, sliderPathVertices[i].X);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
|
||||
/// </summary>
|
||||
public double ComputeRequiredVelocity()
|
||||
{
|
||||
double maximumSlope = 0;
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
|
||||
double timeDifference = vertices[i].Time - vertices[i - 1].Time;
|
||||
|
||||
// A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
|
||||
if (Precision.AlmostEquals(timeDifference, 0))
|
||||
continue;
|
||||
|
||||
maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
|
||||
}
|
||||
|
||||
return maximumSlope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
|
||||
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
|
||||
///
|
||||
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
|
||||
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
|
||||
/// </summary>
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY)
|
||||
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
|
||||
{
|
||||
const float margin = 1;
|
||||
|
||||
// Note: these two variables and `sliderPath` are modified by the local functions.
|
||||
double currentDistance = 0;
|
||||
double currentTime = 0;
|
||||
Vector2 lastPosition = new Vector2(vertices[0].X, 0);
|
||||
|
||||
sliderPath.ControlPoints.Clear();
|
||||
@@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||
|
||||
float deltaX = vertices[i].X - lastPosition.X;
|
||||
double length = vertices[i].Distance - currentDistance;
|
||||
double length = (vertices[i].Time - currentTime) * velocity;
|
||||
|
||||
// Should satisfy `deltaX^2 + deltaY^2 = length^2`.
|
||||
// By invariants, the expression inside the `sqrt` is (almost) non-negative.
|
||||
// The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
|
||||
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
|
||||
|
||||
// When `deltaY` is small, one segment is always enough.
|
||||
@@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Vector2 nextPosition = new Vector2(nextX, nextY);
|
||||
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
|
||||
currentDistance += Vector2.Distance(lastPosition, nextPosition);
|
||||
currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
|
||||
lastPosition = nextPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted.
|
||||
/// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
|
||||
/// </summary>
|
||||
private int vertexIndexAtDistance(double distance)
|
||||
private int vertexIndexAtTime(double time)
|
||||
{
|
||||
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity));
|
||||
// The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
|
||||
int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
|
||||
return i < 0 ? ~i : i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>.
|
||||
/// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
|
||||
/// </summary>
|
||||
private float positionAtDistance(double distance, int index)
|
||||
private float positionAtTime(double time, int index)
|
||||
{
|
||||
if (index <= 0)
|
||||
return vertices[0].X;
|
||||
if (index >= vertices.Count)
|
||||
return vertices[^1].X;
|
||||
|
||||
double length = vertices[index].Distance - vertices[index - 1].Distance;
|
||||
if (Precision.AlmostEquals(length, 0))
|
||||
double duration = vertices[index].Time - vertices[index - 1].Time;
|
||||
if (Precision.AlmostEquals(duration, 0))
|
||||
return vertices[index].X;
|
||||
|
||||
float deltaX = vertices[index].X - vertices[index - 1].X;
|
||||
|
||||
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the two vertices can connected directly while satisfying the slope condition.
|
||||
/// </summary>
|
||||
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
|
||||
{
|
||||
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
|
||||
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
|
||||
return xDistance <= length + allowance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
|
||||
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
|
||||
/// </summary>
|
||||
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
|
||||
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
|
||||
{
|
||||
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
|
||||
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
|
||||
return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
|
||||
}
|
||||
|
||||
private void invalidate() => InvalidationID++;
|
||||
|
||||
@@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
|
||||
{
|
||||
public readonly double Distance;
|
||||
public readonly double Time;
|
||||
|
||||
public readonly float X;
|
||||
|
||||
public JuiceStreamPathVertex(double distance, float x)
|
||||
public JuiceStreamPathVertex(double time, float x)
|
||||
{
|
||||
Distance = distance;
|
||||
Time = time;
|
||||
X = x;
|
||||
}
|
||||
|
||||
public int CompareTo(JuiceStreamPathVertex other)
|
||||
{
|
||||
int c = Distance.CompareTo(other.Distance);
|
||||
int c = Time.CompareTo(other.Time);
|
||||
return c != 0 ? c : X.CompareTo(other.X);
|
||||
}
|
||||
|
||||
public override string ToString() => $"({Distance}, {X})";
|
||||
public override string ToString() => $"({Time}, {X})";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -97,12 +97,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
@@ -45,6 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap),
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -5,7 +5,10 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
@@ -52,8 +55,29 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = result.Playfield as Column;
|
||||
if (result.Playfield is Column col)
|
||||
{
|
||||
// Apply an offset to better align with the visual grid.
|
||||
// This should only be applied during placement, as during selection / drag operations the movement is relative
|
||||
// to the initial point of interaction rather than the grid.
|
||||
switch (col.ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight(col) / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = col;
|
||||
}
|
||||
}
|
||||
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
resultPlayfield.ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
resultPlayfield.ToScreenSpace(Vector2.Zero).Y;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -56,28 +55,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
|
||||
switch (ScrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Up:
|
||||
result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getNoteHeight() =>
|
||||
Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
|
||||
Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
|
||||
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
{
|
||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -113,7 +113,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public void TestCursorInCentre()
|
||||
{
|
||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position)));
|
||||
assertSnappedDistance(0);
|
||||
assertSnappedDistance(beat_length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCursorAlmostInCentre()
|
||||
{
|
||||
AddStep("move mouse to almost centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position) + new Vector2(1)));
|
||||
assertSnappedDistance(beat_length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,114 +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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleAnimationDisable()
|
||||
{
|
||||
HitCircle hitCircle = null;
|
||||
DrawableHitCircle drawableHitCircle = null;
|
||||
|
||||
AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
|
||||
toggleAnimations(true);
|
||||
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||
|
||||
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||
assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
|
||||
|
||||
AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
|
||||
toggleAnimations(false);
|
||||
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||
|
||||
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||
assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
|
||||
AddAssert("hit circle has longer fade-out applied", () =>
|
||||
{
|
||||
var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderAnimationDisable()
|
||||
{
|
||||
Slider slider = null;
|
||||
DrawableSlider drawableSlider = null;
|
||||
DrawableSliderRepeat sliderRepeat = null;
|
||||
|
||||
AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
|
||||
toggleAnimations(true);
|
||||
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||
|
||||
retrieveDrawables();
|
||||
assertFutureTransforms(() => sliderRepeat, true);
|
||||
|
||||
AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
|
||||
toggleAnimations(false);
|
||||
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||
|
||||
retrieveDrawables();
|
||||
assertFutureTransforms(() => sliderRepeat.Arrow, false);
|
||||
seekSmoothlyTo(() => slider.GetEndTime());
|
||||
AddAssert("slider has longer fade-out applied", () =>
|
||||
{
|
||||
var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||
});
|
||||
|
||||
void retrieveDrawables() =>
|
||||
AddStep("retrieve drawables", () =>
|
||||
{
|
||||
drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
|
||||
sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType<SliderRepeat>().First());
|
||||
});
|
||||
}
|
||||
|
||||
private HitCircle getHitCircle(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
|
||||
|
||||
private Slider getSliderWithRepeats(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
|
||||
|
||||
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
|
||||
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
|
||||
|
||||
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
|
||||
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
|
||||
|
||||
private void toggleAnimations(bool enabled)
|
||||
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
|
||||
|
||||
private void seekSmoothlyTo(Func<double> targetTime)
|
||||
{
|
||||
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
|
||||
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
|
||||
}
|
||||
|
||||
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
|
||||
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
|
||||
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
MuteComboCount = { Value = 0 },
|
||||
},
|
||||
PassCondition = () => Beatmap.Value.Track.AggregateVolume.Value == 0.0 &&
|
||||
Player.ChildrenOfType<Metronome>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
|
||||
Player.ChildrenOfType<MetronomeBeat>().SingleOrDefault()?.AggregateVolume.Value == 1.0,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -122,6 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
new OsuModEasy(),
|
||||
new OsuModHardRock(),
|
||||
new OsuModFlashlight(),
|
||||
new MultiMod(new OsuModFlashlight(), new OsuModHidden())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,9 +219,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModHidden))
|
||||
flashlightValue *= 1.3;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (effectiveMissCount > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -85,6 +86,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
setDistances(clockRate);
|
||||
}
|
||||
|
||||
public double OpacityAt(double time, bool hidden)
|
||||
{
|
||||
if (time > BaseObject.StartTime)
|
||||
{
|
||||
// Consider a hitobject as being invisible when its start time is passed.
|
||||
// In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
||||
// but this is an approximation and such a case is unlikely to be hit where this function is used.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double fadeInStartTime = BaseObject.StartTime - BaseObject.TimePreempt;
|
||||
double fadeInDuration = BaseObject.TimeFadeIn;
|
||||
|
||||
if (hidden)
|
||||
{
|
||||
// Taken from OsuModHidden.
|
||||
double fadeOutStartTime = BaseObject.StartTime - BaseObject.TimePreempt + BaseObject.TimeFadeIn;
|
||||
double fadeOutDuration = BaseObject.TimePreempt * OsuModHidden.FADE_OUT_DURATION_MULTIPLIER;
|
||||
|
||||
return Math.Min
|
||||
(
|
||||
Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0),
|
||||
1.0 - Math.Clamp((time - fadeOutStartTime) / fadeOutDuration, 0.0, 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
|
||||
}
|
||||
|
||||
private void setDistances(double clockRate)
|
||||
{
|
||||
if (BaseObject is Slider currentSlider)
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@@ -17,13 +19,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
public Flashlight(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
hidden = mods.Any(m => m is OsuModHidden);
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.07;
|
||||
private double skillMultiplier => 0.05;
|
||||
private double strainDecayBase => 0.15;
|
||||
protected override double DecayWeight => 1.0;
|
||||
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
|
||||
|
||||
private readonly bool hidden;
|
||||
|
||||
private const double max_opacity_bonus = 0.4;
|
||||
private const double hidden_bonus = 0.2;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double strainValueOf(DifficultyHitObject current)
|
||||
@@ -61,13 +69,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
||||
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
||||
|
||||
result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
// Bonus based on how visible the object is.
|
||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||
|
||||
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
}
|
||||
|
||||
lastObj = currentObj;
|
||||
}
|
||||
|
||||
return Math.Pow(smallDistNerf * result, 2.0);
|
||||
result = Math.Pow(smallDistNerf * result, 2.0);
|
||||
|
||||
// Additional bonus for Hidden due to there being no approach circles.
|
||||
if (hidden)
|
||||
result *= 1.0 + hidden_bonus;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||
{
|
||||
public class HitCircleOverlapMarker : BlueprintPiece<HitCircle>
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||
/// </summary>
|
||||
public const double FADE_OUT_EXTENSION = 700;
|
||||
|
||||
private readonly RingPiece ring;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; }
|
||||
|
||||
public HitCircleOverlapMarker()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
},
|
||||
ring = new RingPiece
|
||||
{
|
||||
BorderThickness = 4,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public override void UpdateFrom(HitCircle hitObject)
|
||||
{
|
||||
base.UpdateFrom(hitObject);
|
||||
|
||||
Scale = new Vector2(hitObject.Scale);
|
||||
|
||||
if (hitObject is IHasComboInformation combo)
|
||||
ring.BorderColour = combo.GetComboColour(skin);
|
||||
|
||||
double editorTime = editorClock.CurrentTime;
|
||||
double hitObjectTime = hitObject.StartTime;
|
||||
bool hasReachedObject = editorTime >= hitObjectTime;
|
||||
|
||||
if (hasReachedObject)
|
||||
{
|
||||
float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In);
|
||||
float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
|
||||
|
||||
ring.Scale = new Vector2(1 + 0.1f * ringScale);
|
||||
Alpha = 0.9f * (1 - alpha);
|
||||
}
|
||||
else
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
// intentional no op so we are not hidden when not selected.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -14,11 +15,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject;
|
||||
|
||||
protected readonly HitCirclePiece CirclePiece;
|
||||
private readonly HitCircleOverlapMarker marker;
|
||||
|
||||
public HitCircleSelectionBlueprint(HitCircle circle)
|
||||
: base(circle)
|
||||
{
|
||||
InternalChild = CirclePiece = new HitCirclePiece();
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
marker = new HitCircleOverlapMarker(),
|
||||
CirclePiece = new HitCirclePiece(),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@@ -26,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
base.Update();
|
||||
|
||||
CirclePiece.UpdateFrom(HitObject);
|
||||
marker.UpdateFrom(HitObject);
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
{
|
||||
public abstract class OsuSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>
|
||||
where T : OsuHitObject
|
||||
{
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; }
|
||||
|
||||
protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject;
|
||||
|
||||
protected override bool AlwaysShowWhenSelected => true;
|
||||
|
||||
protected override bool ShouldBeAlive => base.ShouldBeAlive
|
||||
|| (editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|
||||
|
||||
protected OsuSelectionBlueprint(T hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -13,20 +14,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly SliderPosition position;
|
||||
private readonly HitCircleOverlapMarker marker;
|
||||
|
||||
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.position = position;
|
||||
|
||||
InternalChild = CirclePiece = new HitCirclePiece();
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
marker = new HitCircleOverlapMarker(),
|
||||
CirclePiece = new HitCirclePiece(),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle);
|
||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
||||
|
||||
CirclePiece.UpdateFrom(circle);
|
||||
marker.UpdateFrom(circle);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
CirclePiece.Hide();
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
CirclePiece.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
@@ -20,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||
/// </summary>
|
||||
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
|
||||
|
||||
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@@ -37,80 +23,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private class OsuEditorPlayfield : OsuPlayfield
|
||||
{
|
||||
private Bindable<bool> hitAnimations;
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
public OsuEditorPlayfield()
|
||||
{
|
||||
HitPolicy = new AnyOrderHitPolicy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
hitAnimations = config.GetBindable<bool>(OsuSetting.EditorHitAnimations);
|
||||
}
|
||||
|
||||
protected override void OnNewDrawableHitObject(DrawableHitObject d)
|
||||
{
|
||||
d.ApplyCustomUpdateState += updateState;
|
||||
}
|
||||
|
||||
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
if (state == ArmedState.Idle || hitAnimations.Value)
|
||||
return;
|
||||
|
||||
if (hitObject is DrawableHitCircle circle)
|
||||
{
|
||||
using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
|
||||
{
|
||||
circle.ApproachCircle
|
||||
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
|
||||
.Expire();
|
||||
|
||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
if (hitObject is IHasMainCirclePiece mainPieceContainer)
|
||||
{
|
||||
// clear any explode animation logic.
|
||||
// this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
if (hitObject.HitObject == null) return;
|
||||
|
||||
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (hitObject is DrawableSliderRepeat repeat)
|
||||
{
|
||||
repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||
repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||
}
|
||||
|
||||
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableSlider _:
|
||||
case DrawableHitCircle _:
|
||||
// Get the existing fade out transform
|
||||
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
|
||||
|
||||
if (existing == null)
|
||||
return;
|
||||
|
||||
hitObject.RemoveTransform(existing);
|
||||
|
||||
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
|
||||
hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -123,33 +124,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return snapResult;
|
||||
|
||||
return new SnapResult(screenSpacePosition, null);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
||||
return positionSnap;
|
||||
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
}
|
||||
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
|
||||
@@ -27,8 +27,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
|
||||
|
||||
private const double fade_in_duration_multiplier = 0.4;
|
||||
private const double fade_out_duration_multiplier = 0.3;
|
||||
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
|
||||
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
|
||||
|
||||
protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick);
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
static void applyFadeInAdjustment(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier;
|
||||
osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER;
|
||||
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
|
||||
applyFadeInAdjustment(nested);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
static (double fadeStartTime, double fadeDuration) getParameters(OsuHitObject hitObject)
|
||||
{
|
||||
double fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn;
|
||||
double fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier;
|
||||
double fadeOutDuration = hitObject.TimePreempt * FADE_OUT_DURATION_MULTIPLIER;
|
||||
|
||||
// new duration from completed fade in to end (before fading out)
|
||||
double longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime;
|
||||
|
||||
@@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Overlays.Add(new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
drawableRuleset.Overlays.Add(new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -195,16 +195,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
public void UpdateProgress(double completionProgress)
|
||||
{
|
||||
var newPos = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
||||
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
|
||||
|
||||
var diff = lastPosition.HasValue ? lastPosition.Value - newPos : newPos - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
||||
if (diff == Vector2.Zero)
|
||||
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
|
||||
|
||||
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
|
||||
if (diff.LengthFast < 0.01f)
|
||||
return;
|
||||
|
||||
Position = newPos;
|
||||
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
|
||||
|
||||
lastPosition = newPos;
|
||||
lastPosition = Position;
|
||||
}
|
||||
|
||||
private class FollowCircleContainer : CircularContainer
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
|
||||
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
[Resolved(canBeNull: true)] // Can't really be null but required to handle potential of disposal before DI completes.
|
||||
private DrawableHitObject? drawableObject { get; set; }
|
||||
|
||||
[Resolved]
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
|
||||
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(1.9971301024093662d, 200, "diffcalc-test")]
|
||||
[TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
|
||||
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(3.1645810961313674d, 200, "diffcalc-test")]
|
||||
[TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects special hit object patterns which are easier to hit using special techniques
|
||||
/// than normally assumed in the fully-alternating play style.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This component detects two basic types of patterns, leveraged by the following techniques:
|
||||
/// <list>
|
||||
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
|
||||
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public class StaminaCheeseDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
|
||||
/// </summary>
|
||||
private const int roll_min_repetitions = 12;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
|
||||
/// </summary>
|
||||
private const int tl_min_repetitions = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
|
||||
/// </summary>
|
||||
private readonly List<TaikoDifficultyHitObject> hitObjects;
|
||||
|
||||
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
this.hitObjects = hitObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
|
||||
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
|
||||
/// </summary>
|
||||
public void FindCheese()
|
||||
{
|
||||
findRolls(3);
|
||||
findRolls(4);
|
||||
|
||||
findTlTap(0, HitType.Rim);
|
||||
findTlTap(1, HitType.Rim);
|
||||
findTlTap(0, HitType.Centre);
|
||||
findTlTap(1, HitType.Centre);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all sequences hittable using a roll.
|
||||
/// </summary>
|
||||
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
|
||||
private void findRolls(int patternLength)
|
||||
{
|
||||
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
|
||||
|
||||
// for convenience, we're tracking the index of the item *before* our suspected repeat's start,
|
||||
// as that index can be simply subtracted from the current index to get the number of elements in between
|
||||
// without off-by-one errors
|
||||
int indexBeforeLastRepeat = -1;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; i++)
|
||||
{
|
||||
history.Enqueue(hitObjects[i]);
|
||||
if (!history.Full)
|
||||
continue;
|
||||
|
||||
if (!containsPatternRepeat(history, patternLength))
|
||||
{
|
||||
// we're setting this up for the next iteration, hence the +1.
|
||||
// right here this index will point at the queue's front (oldest item),
|
||||
// but that item is about to be popped next loop with an enqueue.
|
||||
indexBeforeLastRepeat = i - history.Count + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int repeatedLength = i - indexBeforeLastRepeat;
|
||||
if (repeatedLength < roll_min_repetitions)
|
||||
continue;
|
||||
|
||||
markObjectsAsCheese(Math.Max(lastMarkEnd, i - repeatedLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
|
||||
/// </summary>
|
||||
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
|
||||
{
|
||||
for (int j = 0; j < patternLength; j++)
|
||||
{
|
||||
if (history[j].HitType != history[j + patternLength].HitType)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and marks all sequences hittable using a TL tap.
|
||||
/// </summary>
|
||||
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
|
||||
/// <param name="type">The type of hit to check for TL taps.</param>
|
||||
private void findTlTap(int parity, HitType type)
|
||||
{
|
||||
int tlLength = -2;
|
||||
int lastMarkEnd = 0;
|
||||
|
||||
for (int i = parity; i < hitObjects.Count; i += 2)
|
||||
{
|
||||
if (hitObjects[i].HitType == type)
|
||||
tlLength += 2;
|
||||
else
|
||||
tlLength = -2;
|
||||
|
||||
if (tlLength < tl_min_repetitions)
|
||||
continue;
|
||||
|
||||
markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i);
|
||||
lastMarkEnd = i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all objects from <paramref name="start"/> to <paramref name="end"/> (inclusive) as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||
/// </summary>
|
||||
private void markObjectsAsCheese(int start, int end)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
hitObjects[i].StaminaCheese = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Stamina of a single key, calculated based on repetition speed.
|
||||
/// </summary>
|
||||
public class SingleKeyStamina
|
||||
{
|
||||
private double? previousHitTime;
|
||||
|
||||
/// <summary>
|
||||
/// Similar to <see cref="StrainDecaySkill.StrainValueOf"/>
|
||||
/// </summary>
|
||||
public double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (previousHitTime == null)
|
||||
{
|
||||
previousHitTime = current.StartTime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double objectStrain = 0.5;
|
||||
objectStrain += speedBonus(current.StartTime - previousHitTime.Value);
|
||||
previousHitTime = current.StartTime;
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the same key.</param>
|
||||
private double speedBonus(double notePairDuration)
|
||||
{
|
||||
return 175 / (notePairDuration + 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@@ -22,39 +20,52 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
|
||||
/// </summary>
|
||||
private const int max_history_length = 2;
|
||||
private readonly SingleKeyStamina[] centreKeyStamina =
|
||||
{
|
||||
new SingleKeyStamina(),
|
||||
new SingleKeyStamina()
|
||||
};
|
||||
|
||||
private readonly SingleKeyStamina[] rimKeyStamina =
|
||||
{
|
||||
new SingleKeyStamina(),
|
||||
new SingleKeyStamina()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
|
||||
/// Current index into <see cref="centreKeyStamina" /> for a centre hit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
|
||||
/// This naturally translates onto index offsets of the objects in the map.
|
||||
/// </remarks>
|
||||
private readonly int hand;
|
||||
private int centreKeyIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
|
||||
/// Current index into <see cref="rimKeyStamina" /> for a rim hit.
|
||||
/// </summary>
|
||||
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
|
||||
|
||||
/// <summary>
|
||||
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
|
||||
/// </summary>
|
||||
private double offhandObjectDuration = double.MaxValue;
|
||||
private int rimKeyIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Stamina"/> skill.
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
|
||||
public Stamina(Mod[] mods, bool rightHand)
|
||||
public Stamina(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
hand = rightHand ? 1 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the next <see cref="SingleKeyStamina"/> to use for the given <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/>.</param>
|
||||
private SingleKeyStamina getNextSingleKeyStamina(TaikoDifficultyHitObject current)
|
||||
{
|
||||
// Alternate key for the same color.
|
||||
if (current.HitType == HitType.Centre)
|
||||
{
|
||||
centreKeyIndex = (centreKeyIndex + 1) % 2;
|
||||
return centreKeyStamina[centreKeyIndex];
|
||||
}
|
||||
|
||||
rimKeyIndex = (rimKeyIndex + 1) % 2;
|
||||
return rimKeyStamina[rimKeyIndex];
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
@@ -65,52 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
}
|
||||
|
||||
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||
|
||||
if (hitObject.ObjectIndex % 2 == hand)
|
||||
{
|
||||
double objectStrain = 1;
|
||||
|
||||
if (hitObject.ObjectIndex == 1)
|
||||
return 1;
|
||||
|
||||
notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
|
||||
|
||||
double shortestRecentNote = notePairDurationHistory.Min();
|
||||
objectStrain += speedBonus(shortestRecentNote);
|
||||
|
||||
if (hitObject.StaminaCheese)
|
||||
objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
|
||||
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
offhandObjectDuration = hitObject.DeltaTime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||
private double cheesePenalty(double notePairDuration)
|
||||
{
|
||||
if (notePairDuration > 125) return 1;
|
||||
if (notePairDuration < 100) return 0.6;
|
||||
|
||||
return 0.6 + (notePairDuration - 100) * 0.016;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
|
||||
/// </summary>
|
||||
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||
private double speedBonus(double notePairDuration)
|
||||
{
|
||||
if (notePairDuration >= 200) return 0;
|
||||
|
||||
double bonus = 200 - notePairDuration;
|
||||
bonus *= bonus;
|
||||
return bonus / 100000;
|
||||
return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
private const double rhythm_skill_multiplier = 0.014;
|
||||
private const double colour_skill_multiplier = 0.01;
|
||||
private const double stamina_skill_multiplier = 0.02;
|
||||
private const double stamina_skill_multiplier = 0.021;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -33,8 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
new Colour(mods),
|
||||
new Rhythm(mods),
|
||||
new Stamina(mods, true),
|
||||
new Stamina(mods, false),
|
||||
new Stamina(mods)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
@@ -58,7 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
);
|
||||
}
|
||||
|
||||
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
|
||||
return taikoDifficultyHitObjects;
|
||||
}
|
||||
|
||||
@@ -69,17 +67,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
var colour = (Colour)skills[0];
|
||||
var rhythm = (Rhythm)skills[1];
|
||||
var staminaRight = (Stamina)skills[2];
|
||||
var staminaLeft = (Stamina)skills[3];
|
||||
var stamina = (Stamina)skills[2];
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
|
||||
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
|
||||
staminaRating *= staminaPenalty;
|
||||
|
||||
double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
|
||||
//TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance.
|
||||
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05)
|
||||
{
|
||||
staminaPenalty *= 0.25;
|
||||
}
|
||||
|
||||
double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina, staminaPenalty);
|
||||
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
|
||||
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
||||
starRating = rescale(starRating);
|
||||
@@ -127,20 +130,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||
/// </remarks>
|
||||
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
|
||||
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina, double staminaPenalty)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||
var staminaRightPeaks = staminaRight.GetCurrentStrainPeaks().ToList();
|
||||
var staminaLeftPeaks = staminaLeft.GetCurrentStrainPeaks().ToList();
|
||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||
|
||||
for (int i = 0; i < colourPeaks.Count; i++)
|
||||
{
|
||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||
double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * staminaPenalty;
|
||||
|
||||
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0;
|
||||
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= lengthBonus;
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -863,5 +863,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decoded.Difficulty.OverallDifficulty, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyAdjacentImplicitCatmullSegmentsAreMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||
|
||||
Assert.That(controlPoints.Count, Is.EqualTo(6));
|
||||
Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonLegacyAdjacentImplicitCatmullSegmentsAreNotMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("adjacent-catmull-segments.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||
|
||||
Assert.That(controlPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[3].Type, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyDuplicateInitialCatmullPointIsMerged()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("catmull-duplicate-initial-controlpoint.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream);
|
||||
var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints;
|
||||
|
||||
Assert.That(controlPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull));
|
||||
Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero));
|
||||
Assert.That(controlPoints[1].Type, Is.Null);
|
||||
Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,37 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddFileToAsyncImportedBeatmap()
|
||||
{
|
||||
RunTestWithRealm((realm, storage) =>
|
||||
{
|
||||
BeatmapSetInfo? detachedSet = null;
|
||||
|
||||
using (var importer = new BeatmapModelManager(realm, storage))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
beatmapSet = await importer.Import(reader);
|
||||
|
||||
Assert.NotNull(beatmapSet);
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
// Intentionally detach on async thread as to not trigger a refresh on the main thread.
|
||||
beatmapSet.PerformRead(s => detachedSet = s.Detach());
|
||||
}).WaitSafely();
|
||||
|
||||
Debug.Assert(detachedSet != null);
|
||||
importer.AddFile(detachedSet, new MemoryStream(), "test");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportBeatmapThenCleanup()
|
||||
{
|
||||
@@ -476,7 +507,7 @@ namespace osu.Game.Tests.Database
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.CreateFileSafely(firstFile.File.GetStoragePath()))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
{
|
||||
public class ModSettingsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModSettingsUnboundWhenCopied()
|
||||
{
|
||||
var original = new OsuModDoubleTime();
|
||||
var copy = (OsuModDoubleTime)original.DeepClone();
|
||||
|
||||
original.SpeedChange.Value = 2;
|
||||
|
||||
Assert.That(original.SpeedChange.Value, Is.EqualTo(2.0));
|
||||
Assert.That(copy.SpeedChange.Value, Is.EqualTo(1.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiModSettingsUnboundWhenCopied()
|
||||
{
|
||||
var original = new MultiMod(new OsuModDoubleTime());
|
||||
var copy = (MultiMod)original.DeepClone();
|
||||
|
||||
((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2;
|
||||
|
||||
Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
|
||||
Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
{
|
||||
public class TestCustomisableModRuleset : Ruleset
|
||||
{
|
||||
public static RulesetInfo CreateTestRulesetInfo() => new TestCustomisableModRuleset().RulesetInfo;
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
if (type == ModType.Conversion)
|
||||
{
|
||||
return new Mod[]
|
||||
{
|
||||
new TestModCustomisable1(),
|
||||
new TestModCustomisable2()
|
||||
};
|
||||
}
|
||||
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
|
||||
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException();
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
|
||||
|
||||
public override string Description { get; } = "test";
|
||||
public override string ShortName { get; } = "tst";
|
||||
|
||||
public class TestModCustomisable1 : TestModCustomisable
|
||||
{
|
||||
public override string Name => "Customisable Mod 1";
|
||||
|
||||
public override string Acronym => "CM1";
|
||||
}
|
||||
|
||||
public class TestModCustomisable2 : TestModCustomisable
|
||||
{
|
||||
public override string Name => "Customisable Mod 2";
|
||||
|
||||
public override string Acronym => "CM2";
|
||||
|
||||
public override bool RequiresConfiguration => true;
|
||||
}
|
||||
|
||||
public abstract class TestModCustomisable : Mod, IApplicableMod
|
||||
{
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
public override string Description => "This is a customisable test mod.";
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
[SettingSource("Sample float", "Change something for a mod")]
|
||||
public BindableFloat SliderBindable { get; } = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 7
|
||||
};
|
||||
|
||||
[SettingSource("Sample bool", "Clicking this changes a setting")]
|
||||
public BindableBool TickBindable { get; } = new BindableBool();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Mods
|
||||
{
|
||||
@@ -16,11 +18,14 @@ namespace osu.Game.Tests.Rulesets.Mods
|
||||
private const double duration = 9000;
|
||||
|
||||
private TrackVirtual track;
|
||||
private OsuPlayfield playfield;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
track = new TrackVirtual(20_000);
|
||||
// define a fake playfield to re-calculate the current rate by ModTimeRamp.Update(Playfield).
|
||||
playfield = new OsuPlayfield { Clock = new FramedClock(track) };
|
||||
}
|
||||
|
||||
[TestCase(0, 1)]
|
||||
@@ -80,8 +85,8 @@ namespace osu.Game.Tests.Rulesets.Mods
|
||||
private void seekTrackAndUpdateMod(ModTimeRamp mod, double time)
|
||||
{
|
||||
track.Seek(time);
|
||||
// update the mod via a fake playfield to re-calculate the current rate.
|
||||
mod.Update(null);
|
||||
playfield.Clock.ProcessFrame();
|
||||
mod.Update(playfield);
|
||||
}
|
||||
|
||||
private static Beatmap createSingleSpinnerBeatmap()
|
||||
|
||||
@@ -84,20 +84,26 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
explicitMap.Title = someDifficulties.TitleUnicode = "explicit beatmap";
|
||||
explicitMap.HasExplicitContent = true;
|
||||
|
||||
var spotlightMap = CreateAPIBeatmapSet(Ruleset.Value);
|
||||
spotlightMap.Title = someDifficulties.TitleUnicode = "spotlight beatmap";
|
||||
spotlightMap.FeaturedInSpotlight = true;
|
||||
|
||||
var featuredMap = CreateAPIBeatmapSet(Ruleset.Value);
|
||||
featuredMap.Title = someDifficulties.TitleUnicode = "featured artist beatmap";
|
||||
featuredMap.TrackId = 1;
|
||||
|
||||
var explicitFeaturedMap = CreateAPIBeatmapSet(Ruleset.Value);
|
||||
explicitFeaturedMap.Title = someDifficulties.TitleUnicode = "explicit featured artist";
|
||||
explicitFeaturedMap.HasExplicitContent = true;
|
||||
explicitFeaturedMap.TrackId = 2;
|
||||
var allBadgesMap = CreateAPIBeatmapSet(Ruleset.Value);
|
||||
allBadgesMap.Title = someDifficulties.TitleUnicode = "all-badges beatmap";
|
||||
allBadgesMap.HasExplicitContent = true;
|
||||
allBadgesMap.FeaturedInSpotlight = true;
|
||||
allBadgesMap.TrackId = 2;
|
||||
|
||||
var longName = CreateAPIBeatmapSet(Ruleset.Value);
|
||||
longName.Title = longName.TitleUnicode = "this track has an incredibly and implausibly long title";
|
||||
longName.Artist = longName.ArtistUnicode = "and this artist! who would have thunk it. it's really such a long name.";
|
||||
longName.Source = "wow. even the source field has an impossibly long string in it. this really takes the cake, doesn't it?";
|
||||
longName.HasExplicitContent = true;
|
||||
longName.FeaturedInSpotlight = true;
|
||||
longName.TrackId = 444;
|
||||
|
||||
testCases = new[]
|
||||
@@ -108,8 +114,9 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
someDifficulties,
|
||||
manyDifficulties,
|
||||
explicitMap,
|
||||
spotlightMap,
|
||||
featuredMap,
|
||||
explicitFeaturedMap,
|
||||
allBadgesMap,
|
||||
longName
|
||||
};
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
@@ -23,7 +25,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private BindableBeatDivisor bindableBeatDivisor;
|
||||
|
||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType<EquilateralTriangle>().Single();
|
||||
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@@ -47,6 +48,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
(typeof(EditorBeatmap), editorBeatmap),
|
||||
(typeof(IBeatSnapProvider), editorBeatmap),
|
||||
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Green)),
|
||||
},
|
||||
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
|
||||
};
|
||||
|
||||
@@ -185,10 +185,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private class SnapProvider : IDistanceSnapProvider
|
||||
{
|
||||
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||
new SnapResult(screenSpacePosition, null);
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.Grids) => new SnapResult(screenSpacePosition, 0);
|
||||
|
||||
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@@ -13,6 +16,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneEditorClock : EditorClockTestScene
|
||||
{
|
||||
[Cached]
|
||||
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
|
||||
|
||||
public TestSceneEditorClock()
|
||||
{
|
||||
Add(new FillFlowContainer
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneEditorMenuBar : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
public TestSceneEditorMenuBar()
|
||||
{
|
||||
Add(new Container
|
||||
|
||||
@@ -28,6 +28,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSeekToFirst()
|
||||
{
|
||||
pressAndCheckTime(Key.Z, 2170);
|
||||
pressAndCheckTime(Key.Z, 0);
|
||||
pressAndCheckTime(Key.Z, 2170);
|
||||
|
||||
AddAssert("track not running", () => !EditorClock.IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRestart()
|
||||
{
|
||||
pressAndCheckTime(Key.V, 227170);
|
||||
|
||||
AddAssert("track not running", () => !EditorClock.IsRunning);
|
||||
|
||||
AddStep("press X", () => InputManager.Key(Key.X));
|
||||
|
||||
AddAssert("track running", () => EditorClock.IsRunning);
|
||||
AddAssert("time restarted", () => EditorClock.CurrentTime < 100000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseResume()
|
||||
{
|
||||
AddAssert("track not running", () => !EditorClock.IsRunning);
|
||||
|
||||
AddStep("press C", () => InputManager.Key(Key.C));
|
||||
AddAssert("track running", () => EditorClock.IsRunning);
|
||||
|
||||
AddStep("press C", () => InputManager.Key(Key.C));
|
||||
AddAssert("track not running", () => !EditorClock.IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSeekToLast()
|
||||
{
|
||||
pressAndCheckTime(Key.V, 227170);
|
||||
pressAndCheckTime(Key.V, 229170);
|
||||
pressAndCheckTime(Key.V, 227170);
|
||||
|
||||
AddAssert("track not running", () => !EditorClock.IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSnappedSeeking()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -66,6 +67,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHasFocus()
|
||||
{
|
||||
clickDifficultyPiece(0);
|
||||
velocityPopoverHasFocus();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleSelection()
|
||||
{
|
||||
@@ -133,6 +141,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void velocityPopoverHasFocus() => AddUntilStep("velocity popover textbox focused", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
|
||||
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
|
||||
var textbox = slider?.ChildrenOfType<OsuTextBox>().Single();
|
||||
|
||||
return textbox?.HasFocus == true;
|
||||
});
|
||||
|
||||
private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
|
||||
@@ -151,6 +168,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private void dismissPopover()
|
||||
{
|
||||
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
|
||||
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHasFocus()
|
||||
{
|
||||
clickSamplePiece(0);
|
||||
samplePopoverHasFocus();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleSelection()
|
||||
{
|
||||
@@ -173,14 +180,23 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
samplePopoverHasSingleBank("normal");
|
||||
}
|
||||
|
||||
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
|
||||
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
|
||||
{
|
||||
var difficultyPiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
|
||||
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
|
||||
|
||||
InputManager.MoveMouseTo(difficultyPiece);
|
||||
InputManager.MoveMouseTo(samplePiece);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void samplePopoverHasFocus() => AddUntilStep("sample popover textbox focused", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
|
||||
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
|
||||
var textbox = slider?.ChildrenOfType<OsuTextBox>().Single();
|
||||
|
||||
return textbox?.HasFocus == true;
|
||||
});
|
||||
|
||||
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
|
||||
@@ -215,8 +231,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private void dismissPopover()
|
||||
{
|
||||
AddStep("unfocus textbox", () => InputManager.Key(Key.Escape));
|
||||
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
|
||||
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Any(popover => popover.IsPresent));
|
||||
}
|
||||
|
||||
private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>
|
||||
|
||||
@@ -12,7 +12,7 @@ using osuTK;
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestScenePlaybackControl : OsuTestScene
|
||||
public class TestScenePlaybackControl : EditorClockTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
||||
@@ -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.UserInterfaceV2;
|
||||
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 TestTapThenReset()
|
||||
{
|
||||
AddStep("click tap button", () =>
|
||||
{
|
||||
control.ChildrenOfType<RoundedButton>()
|
||||
.Last()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for track playing", () => Clock.IsRunning);
|
||||
|
||||
AddStep("click reset button", () =>
|
||||
{
|
||||
control.ChildrenOfType<RoundedButton>()
|
||||
.First()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("set low bpm", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 1000;
|
||||
});
|
||||
|
||||
AddStep("click tap button", () =>
|
||||
{
|
||||
control.ChildrenOfType<RoundedButton>()
|
||||
.Last()
|
||||
.TriggerClick();
|
||||
});
|
||||
|
||||
AddSliderStep("BPM", 30, 400, 128, bpm =>
|
||||
{
|
||||
if (editorBeatmap == null)
|
||||
return;
|
||||
|
||||
editorBeatmap.ControlPointInfo.TimingPoints.First().BeatLength = 60000f / bpm;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -14,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osu.Game.Tests.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Recorder = recorder = new TestReplayRecorder(new Score
|
||||
{
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
|
||||
{
|
||||
|
||||
@@ -1,57 +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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneReplaySettingsOverlay : OsuTestScene
|
||||
{
|
||||
public TestSceneReplaySettingsOverlay()
|
||||
{
|
||||
ExampleContainer container;
|
||||
|
||||
Add(new PlayerSettingsOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
State = { Value = Visibility.Visible }
|
||||
});
|
||||
|
||||
Add(container = new ExampleContainer());
|
||||
|
||||
AddStep(@"Add button", () => container.Add(new TriangleButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = @"Button",
|
||||
}));
|
||||
|
||||
AddStep(@"Add checkbox", () => container.Add(new PlayerCheckbox
|
||||
{
|
||||
LabelText = "Checkbox",
|
||||
}));
|
||||
|
||||
AddStep(@"Add textbox", () => container.Add(new FocusedTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 30,
|
||||
PlaceholderText = "Textbox",
|
||||
HoldFocus = false,
|
||||
}));
|
||||
}
|
||||
|
||||
private class ExampleContainer : PlayerSettingsGroup
|
||||
{
|
||||
public ExampleContainer()
|
||||
: base("example")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Mods;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
recordingManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Recorder = recorder = new TestReplayRecorder
|
||||
{
|
||||
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
playbackManager = new TestRulesetInputManager(TestCustomisableModRuleset.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
Clock = new FramedClock(manualClock),
|
||||
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
|
||||
|
||||
@@ -124,13 +124,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Status = { Value = new RoomStatusOpen() },
|
||||
Category = { Value = RoomCategory.Spotlight },
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = { Value = "Featured artist room" },
|
||||
Status = { Value = new RoomStatusOpen() },
|
||||
Category = { Value = RoomCategory.FeaturedArtist },
|
||||
}),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 5);
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 6);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,21 +1,121 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneFreeModSelectOverlay : MultiplayerTestScene
|
||||
{
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
private FreeModSelectOverlay freeModSelectOverlay;
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase osuGameBase)
|
||||
{
|
||||
Child = new FreeModSelectOverlay
|
||||
availableMods.BindTo(osuGameBase.AvailableMods);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFreeModSelect()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddUntilStep("all visible mods are playable",
|
||||
() => this.ChildrenOfType<ModPanel>()
|
||||
.Where(panel => panel.IsPresent)
|
||||
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
|
||||
|
||||
AddToggleStep("toggle visibility", visible =>
|
||||
{
|
||||
if (freeModSelectOverlay != null)
|
||||
freeModSelectOverlay.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomisationNotAvailable()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
|
||||
AddWaitStep("wait some", 3);
|
||||
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectDeselectAllViaKeyboard()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
|
||||
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectDeselectAll()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click select all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<SelectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
private void createFreeModSelect()
|
||||
{
|
||||
AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
};
|
||||
});
|
||||
});
|
||||
AddUntilStep("all column content loaded",
|
||||
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
&& freeModSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
|
||||
}
|
||||
|
||||
private bool assertAllAvailableModsSelected()
|
||||
{
|
||||
var allAvailableMods = availableMods.Value
|
||||
.SelectMany(pair => pair.Value)
|
||||
.Where(mod => mod.UserPlayable && mod.HasImplementation)
|
||||
.ToList();
|
||||
|
||||
foreach (var availableMod in allAvailableMods)
|
||||
{
|
||||
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneFreeModSelectScreen : MultiplayerTestScene
|
||||
{
|
||||
private FreeModSelectScreen freeModSelectScreen;
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase osuGameBase)
|
||||
{
|
||||
availableMods.BindTo(osuGameBase.AvailableMods);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFreeModSelect()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddUntilStep("all visible mods are playable",
|
||||
() => this.ChildrenOfType<ModPanel>()
|
||||
.Where(panel => panel.IsPresent)
|
||||
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
|
||||
|
||||
AddToggleStep("toggle visibility", visible =>
|
||||
{
|
||||
if (freeModSelectScreen != null)
|
||||
freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomisationNotAvailable()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("select difficulty adjust", () => freeModSelectScreen.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
|
||||
AddWaitStep("wait some", 3);
|
||||
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectDeselectAll()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddStep("click select all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedButton>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectScreen.SelectedMods.Value.Any());
|
||||
}
|
||||
|
||||
private void createFreeModSelect()
|
||||
{
|
||||
AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
});
|
||||
AddUntilStep("all column content loaded",
|
||||
() => freeModSelectScreen.ChildrenOfType<ModColumn>().Any()
|
||||
&& freeModSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
|
||||
}
|
||||
|
||||
private bool assertAllAvailableModsSelected()
|
||||
{
|
||||
var allAvailableMods = availableMods.Value
|
||||
.SelectMany(pair => pair.Value)
|
||||
.Where(mod => mod.UserPlayable && mod.HasImplementation)
|
||||
.ToList();
|
||||
|
||||
foreach (var availableMod in allAvailableMods)
|
||||
{
|
||||
if (freeModSelectScreen.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@@ -15,13 +16,14 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@@ -349,15 +351,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests spectating with a gameplay start time set to a negative value.
|
||||
/// Simulating beatmaps with high <see cref="BeatmapInfo.AudioLeadIn"/> or negative time storyboard elements.
|
||||
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNegativeGameplayStartTime()
|
||||
public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000);
|
||||
|
||||
/// <summary>
|
||||
/// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestIntroStoryboardElement() => testLeadIn(b =>
|
||||
{
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1);
|
||||
b.Storyboard.GetLayer("Background").Add(sprite);
|
||||
});
|
||||
|
||||
private void testLeadIn(Action<WorkingBeatmap> applyToBeatmap = null)
|
||||
{
|
||||
start(PLAYER_1_ID);
|
||||
|
||||
loadSpectateScreen(false, -500);
|
||||
loadSpectateScreen(false, applyToBeatmap);
|
||||
|
||||
// to ensure negative gameplay start time does not affect spectator, send frames exactly after StartGameplay().
|
||||
// (similar to real spectating sessions in which the first frames get sent between StartGameplay() and player load complete)
|
||||
@@ -371,14 +385,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertRunning(PLAYER_1_ID);
|
||||
}
|
||||
|
||||
private void loadSpectateScreen(bool waitForPlayerLoad = true, double? gameplayStartTime = null)
|
||||
private void loadSpectateScreen(bool waitForPlayerLoad = true, Action<WorkingBeatmap> applyToBeatmap = null)
|
||||
{
|
||||
AddStep(!gameplayStartTime.HasValue ? "load screen" : $"load screen (start = {gameplayStartTime}ms)", () =>
|
||||
AddStep("load screen", () =>
|
||||
{
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
|
||||
Ruleset.Value = importedBeatmap.Ruleset;
|
||||
|
||||
LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(SelectedRoom.Value, playingUsers.ToArray(), gameplayStartTime));
|
||||
applyToBeatmap?.Invoke(Beatmap.Value);
|
||||
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value, playingUsers.ToArray()));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
|
||||
@@ -461,19 +477,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
|
||||
|
||||
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
|
||||
|
||||
private class TestMultiSpectatorScreen : MultiSpectatorScreen
|
||||
{
|
||||
private readonly double? startTime;
|
||||
|
||||
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
|
||||
: base(room, users)
|
||||
{
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
|
||||
=> new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +627,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
|
||||
|
||||
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectScreen>().Single().State.Value == Visibility.Hidden);
|
||||
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
|
||||
|
||||
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private void assertHasFreeModButton(Type type, bool hasButton = true)
|
||||
{
|
||||
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
|
||||
() => this.ChildrenOfType<FreeModSelectScreen>()
|
||||
() => this.ChildrenOfType<FreeModSelectOverlay>()
|
||||
.Single()
|
||||
.ChildrenOfType<ModPanel>()
|
||||
.Where(panel => !panel.Filtered.Value)
|
||||
|
||||
@@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
@@ -21,12 +22,14 @@ using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -171,10 +174,87 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("mod select contents loaded",
|
||||
() => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
|
||||
AddUntilStep("mod select contains only double time mod",
|
||||
() => this.ChildrenOfType<UserModSelectScreen>()
|
||||
() => this.ChildrenOfType<UserModSelectOverlay>()
|
||||
.SingleOrDefault()?
|
||||
.ChildrenOfType<ModPanel>()
|
||||
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectKeyWithAllowedMods()
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) }
|
||||
});
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
|
||||
|
||||
AddUntilStep("mod select shown", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectKeyWithNoAllowedMods()
|
||||
{
|
||||
AddStep("add playlist item with no allowed mods", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
});
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddStep("press toggle mod select key", () => InputManager.Key(Key.F1));
|
||||
|
||||
AddWaitStep("wait some", 3);
|
||||
AddAssert("mod select not shown", () => this.ChildrenOfType<ModSelectOverlay>().Single().State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNextPlaylistItemSelectedAfterCompletion()
|
||||
{
|
||||
AddStep("add two playlist items", () =>
|
||||
{
|
||||
SelectedRoom.Value.Playlist.AddRange(new[]
|
||||
{
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
},
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddStep("change user to loaded", () => MultiplayerClient.ChangeState(MultiplayerUserState.Loaded));
|
||||
AddUntilStep("user playing", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Playing);
|
||||
AddStep("abort gameplay", () => MultiplayerClient.AbortGameplay());
|
||||
|
||||
AddUntilStep("last playlist item selected", () =>
|
||||
{
|
||||
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID);
|
||||
return lastItem.IsSelectedItem;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using NUnit.Framework;
|
||||
@@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
if (isDisposing)
|
||||
return;
|
||||
|
||||
using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create))
|
||||
using (var outStream = LocalStorage.CreateFileSafely(DatabaseContextFactory.DATABASE_NAME))
|
||||
using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME))
|
||||
stream.CopyTo(outStream);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
@@ -33,7 +32,6 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
private IReadOnlyList<Type> requiredGameDependencies => new[]
|
||||
{
|
||||
typeof(OsuGame),
|
||||
typeof(SentryLogger),
|
||||
typeof(OsuLogo),
|
||||
typeof(IdleTracker),
|
||||
typeof(OnScreenDisplay),
|
||||
@@ -42,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
typeof(DashboardOverlay),
|
||||
typeof(NewsOverlay),
|
||||
typeof(ChannelManager),
|
||||
typeof(ChatOverlay),
|
||||
typeof(ChatOverlayV2),
|
||||
typeof(SettingsOverlay),
|
||||
typeof(UserProfileOverlay),
|
||||
typeof(BeatmapSetOverlay),
|
||||
|
||||
@@ -86,9 +86,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestOverlaysAlwaysClosed()
|
||||
{
|
||||
ChatOverlay chat = null;
|
||||
ChatOverlayV2 chat = null;
|
||||
AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType<ChatOverlay>().SingleOrDefault()) != null);
|
||||
AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType<ChatOverlayV2>().SingleOrDefault()) != null);
|
||||
|
||||
AddStep("show chat", () => InputManager.Key(Key.F8));
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
@@ -54,6 +56,39 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaEscapeAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSongSelectBackActionHandling()
|
||||
{
|
||||
TestPlaySongSelect songSelect = null;
|
||||
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
|
||||
AddStep("set filter", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("press back", () => InputManager.Click(MouseButton.Button1));
|
||||
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
|
||||
AddStep("set filter again", () => songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value = "test");
|
||||
AddStep("open collections dropdown", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(songSelect.ChildrenOfType<CollectionFilterDropdown>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("press back once", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("still at song select", () => Game.ScreenStack.CurrentScreen == songSelect);
|
||||
AddAssert("collections dropdown closed", () => songSelect
|
||||
.ChildrenOfType<CollectionFilterDropdown>().Single()
|
||||
.ChildrenOfType<Dropdown<CollectionFilterMenuItem>.DropdownMenu>().Single().State == MenuState.Closed);
|
||||
|
||||
AddStep("press back a second time", () => InputManager.Click(MouseButton.Button1));
|
||||
AddAssert("filter cleared", () => string.IsNullOrEmpty(songSelect.ChildrenOfType<SearchTextBox>().Single().Current.Value));
|
||||
|
||||
AddStep("press back a third time", () => InputManager.Click(MouseButton.Button1));
|
||||
ConfirmAtMainMenu();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
|
||||
/// but should be handled *after* song select).
|
||||
@@ -487,6 +522,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
|
||||
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
// move the mouse firmly inside game bounds to avoid interfering with other tests.
|
||||
AddStep("center cursor", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -568,7 +606,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
public class TestPlaySongSelect : PlaySongSelect
|
||||
{
|
||||
public ModSelectScreen ModSelectOverlay => ModSelect;
|
||||
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
||||
|
||||
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ using osu.Game.Rulesets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
@@ -101,6 +104,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet()));
|
||||
downloadAssert(true);
|
||||
|
||||
AddAssert("status is loved", () => overlay.ChildrenOfType<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Loved);
|
||||
AddAssert("scores container is visible", () => overlay.ChildrenOfType<ScoresContainer>().Single().Alpha == 1);
|
||||
|
||||
AddStep("go to second beatmap", () => overlay.ChildrenOfType<BeatmapPicker.DifficultySelectorButton>().ElementAt(1).TriggerClick());
|
||||
|
||||
AddAssert("status is graveyard", () => overlay.ChildrenOfType<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Graveyard);
|
||||
AddAssert("scores container is hidden", () => overlay.ChildrenOfType<ScoresContainer>().Single().Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -165,6 +176,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpotlightBeatmap()
|
||||
{
|
||||
AddStep("show spotlight map", () =>
|
||||
{
|
||||
var beatmapSet = getBeatmapSet();
|
||||
beatmapSet.FeaturedInSpotlight = true;
|
||||
overlay.ShowBeatmapSet(beatmapSet);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFeaturedBeatmap()
|
||||
{
|
||||
@@ -176,6 +198,19 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllBadgesBeatmap()
|
||||
{
|
||||
AddStep("show map with all badges", () =>
|
||||
{
|
||||
var beatmapSet = getBeatmapSet();
|
||||
beatmapSet.HasExplicitContent = true;
|
||||
beatmapSet.FeaturedInSpotlight = true;
|
||||
beatmapSet.TrackId = 1;
|
||||
overlay.ShowBeatmapSet(beatmapSet);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHide()
|
||||
{
|
||||
@@ -208,6 +243,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
|
||||
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
|
||||
},
|
||||
Status = i % 2 == 0 ? BeatmapOnlineStatus.Graveyard : BeatmapOnlineStatus.Loved,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osu.Game.Overlays.Chat.Listing;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@@ -25,7 +26,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Cached]
|
||||
private readonly Bindable<Channel> selected = new Bindable<Channel>();
|
||||
|
||||
private OsuSpriteText selectorText;
|
||||
private OsuSpriteText selectedText;
|
||||
private OsuSpriteText leaveText;
|
||||
private ChannelList channelList;
|
||||
@@ -43,21 +43,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Height = 0.7f,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, 20),
|
||||
new Dimension(GridSizeMode.Absolute, 20),
|
||||
new Dimension(GridSizeMode.Absolute, 20),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
selectorText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
selectedText = new OsuSpriteText
|
||||
@@ -89,7 +80,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
channelList.OnRequestSelect += channel =>
|
||||
{
|
||||
channelList.SelectorActive.Value = false;
|
||||
selected.Value = channel;
|
||||
};
|
||||
|
||||
@@ -101,12 +91,6 @@ namespace osu.Game.Tests.Visual.Online
|
||||
channelList.RemoveChannel(channel);
|
||||
};
|
||||
|
||||
channelList.SelectorActive.BindValueChanged(change =>
|
||||
{
|
||||
selectorText.Text = $"Channel Selector Active: {change.NewValue}";
|
||||
selected.Value = null;
|
||||
}, true);
|
||||
|
||||
selected.BindValueChanged(change =>
|
||||
{
|
||||
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
|
||||
@@ -135,35 +119,37 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("Unread Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Unread.Value = true;
|
||||
});
|
||||
|
||||
AddStep("Read Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Unread.Value = false;
|
||||
});
|
||||
|
||||
AddStep("Add Mention Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value++;
|
||||
});
|
||||
|
||||
AddStep("Add 98 Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value += 98;
|
||||
});
|
||||
|
||||
AddStep("Clear Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
if (validItem)
|
||||
channelList.GetItem(selected.Value).Mentions.Value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private bool validItem => selected.Value != null && !(selected.Value is ChannelListing.ChannelListingChannel);
|
||||
|
||||
private Channel createRandomPublicChannel()
|
||||
{
|
||||
int id = RNG.Next(0, 10000);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user