1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 14:32:55 +08:00

Merge branch 'master' into fl-opacity

This commit is contained in:
Dan Balasescu 2022-01-20 14:48:20 +09:00
commit 36d1cdb95a
674 changed files with 9837 additions and 20827 deletions

View File

@ -0,0 +1 @@
osu.Android

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu/riderModule.iml" />
</modules>
</component>
</project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="RiderProjectSettingsUpdater"> <component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="1" /> <option name="vcsConfiguration" value="2" />
</component> </component>
</project> </project>

View File

@ -13,3 +13,5 @@ M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.H
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.

View File

@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:** **Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.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+](https://github.com/ppy/osu/releases/latest/download/osu.app.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. - 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.

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase) private void load()
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase) private void load()
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase) private void load()
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -4,7 +4,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase) private void load()
{ {
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Pippidon.UI
private PippidonCharacter pippidon; private PippidonCharacter pippidon;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TextureStore textures) private void load()
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1220.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.118.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.7.1" /> <PackageReference Include="Realm" Version="10.8.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -17,7 +17,7 @@ using osu.Game.Database;
namespace osu.Android namespace osu.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
@ -47,11 +47,6 @@ namespace osu.Android
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
// The default current directory on android is '/'.
// On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage.
// In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory.
System.Environment.CurrentDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
// OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack. // OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack.

View File

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
</manifest> </manifest>

View File

@ -0,0 +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 Android;
using Android.App;
// used for AndroidBatteryInfo
[assembly: UsesPermission(Manifest.Permission.BatteryStats)]

View File

@ -29,6 +29,7 @@
<Compile Include="GameplayScreenRotationLocker.cs" /> <Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" /> <Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" /> <Compile Include="OsuGameAndroid.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Properties\AndroidManifest.xml" /> <None Include="Properties\AndroidManifest.xml" />

View File

@ -10,14 +10,11 @@ using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Win32; using Microsoft.Win32;
using osu.Desktop.Security; using osu.Desktop.Security;
using osu.Desktop.Overlays;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Desktop.Updater; using osu.Desktop.Updater;
using osu.Framework; using osu.Framework;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows; using osu.Desktop.Windows;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -27,13 +24,9 @@ namespace osu.Desktop
{ {
internal class OsuGameDesktop : OsuGame internal class OsuGameDesktop : OsuGame
{ {
private readonly bool noVersionOverlay;
private VersionManager versionManager;
public OsuGameDesktop(string[] args = null) public OsuGameDesktop(string[] args = null)
: base(args) : base(args)
{ {
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
} }
public override StableStorage GetStorageForStableInstall() public override StableStorage GetStorageForStableInstall()
@ -114,9 +107,6 @@ namespace osu.Desktop
{ {
base.LoadComplete(); base.LoadComplete();
if (!noVersionOverlay)
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
LoadComponentAsync(new DiscordRichPresence(), Add); LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
@ -125,23 +115,6 @@ namespace osu.Desktop
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add); LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
} }
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
{
base.ScreenChanged(lastScreen, newScreen);
switch (newScreen)
{
case IntroScreen _:
case MainMenu _:
versionManager?.Show();
break;
default:
versionManager?.Hide();
break;
}
}
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
{ {
base.SetHost(host); base.SetHost(host);

View File

@ -73,10 +73,10 @@ namespace osu.Desktop.Security
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, NotificationOverlay notificationOverlay) private void load(OsuColour colours)
{ {
Icon = FontAwesome.Solid.ShieldAlt; Icon = FontAwesome.Solid.ShieldAlt;
IconBackgound.Colour = colours.YellowDark; IconBackground.Colour = colours.YellowDark;
} }
} }
} }

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// ReSharper disable IdentifierTypo
namespace osu.Desktop.Windows namespace osu.Desktop.Windows
{ {
internal class WindowsKey internal class WindowsKey

View File

@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Android.App; using Android.App;
using Android.Content.PM;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Tests; using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.Android namespace osu.Game.Rulesets.Catch.Tests.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
public class MainActivity : AndroidGameActivity public class MainActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuTestBrowser(); protected override Framework.Game CreateGame() => new OsuTestBrowser();

View File

@ -14,7 +14,6 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";

View File

@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected CatchSelectionBlueprintTestScene() protected CatchSelectionBlueprintTestScene()
{ {
EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } }; EditorBeatmap = new EditorBeatmap(new CatchBeatmap
{
BeatmapInfo =
{
Ruleset = new CatchRuleset().RulesetInfo,
}
}) { Difficulty = { CircleSize = 0 } };
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{ {
BeatLength = 100 BeatLength = 100

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 }, Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
Ruleset = ruleset Ruleset = ruleset
} }
}; };

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, Difficulty = new BeatmapDifficulty { CircleSize = 6 },
Ruleset = ruleset Ruleset = ruleset
} }
}; };

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, Difficulty = new BeatmapDifficulty { CircleSize = 6 },
Ruleset = ruleset Ruleset = ruleset
} }
}; };

View File

@ -35,12 +35,12 @@ namespace osu.Game.Rulesets.Catch.Tests
HitObjects = new List<HitObject> { new Fruit() }, HitObjects = new List<HitObject> { new Fruit() },
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty(), Difficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Artist = @"Unknown", Artist = @"Unknown",
Title = @"You're breathtaking", Title = @"You're breathtaking",
AuthorString = @"Everyone", Author = { Username = @"Everyone" },
}, },
Ruleset = new CatchRuleset().RulesetInfo Ruleset = new CatchRuleset().RulesetInfo
}, },
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
[Test] [Test]
public void TestJuicestream() public void TestJuiceStream()
{ {
AddStep("hit juicestream", () => spawnJuiceStream(true)); AddStep("hit juicestream", () => spawnJuiceStream(true));
AddUntilStep("wait for completion", () => playfieldIsEmpty); AddUntilStep("wait for completion", () => playfieldIsEmpty);

View File

@ -37,20 +37,20 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(_ => createDrawableDroplet(true)));
} }
private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => private Drawable createDrawableFruit(int indexInBeatmap, bool hyperDash = false) =>
new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit
{ {
IndexInBeatmap = indexInBeatmap, IndexInBeatmap = indexInBeatmap,
HyperDashBindable = { Value = hyperdash } HyperDashBindable = { Value = hyperDash }
})); }));
private Drawable createDrawableBanana() => private Drawable createDrawableBanana() =>
new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana())); new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana()));
private Drawable createDrawableDroplet(bool hyperdash = false) => private Drawable createDrawableDroplet(bool hyperDash = false) =>
new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet
{ {
HyperDashBindable = { Value = hyperdash } HyperDashBindable = { Value = hyperDash }
})); }));
private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet())); private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet()));

View File

@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Tests
BeatmapInfo = BeatmapInfo =
{ {
Ruleset = ruleset, Ruleset = ruleset,
BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f } Difficulty = new BeatmapDifficulty { CircleSize = 3.6f }
} }
}; };

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { CircleSize = 5, SliderMultiplier = 2 }, Difficulty = new BeatmapDifficulty { CircleSize = 5, SliderMultiplier = 2 },
Ruleset = ruleset Ruleset = ruleset
}, },
HitObjects = new List<HitObject> HitObjects = new List<HitObject>

View File

@ -0,0 +1,11 @@
// 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;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceAttributes : PerformanceAttributes
{
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
} }
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) public override PerformanceAttributes Calculate()
{ {
mods = Score.Mods; mods = Score.Mods;
@ -44,15 +44,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits(); int numTotalHits = totalComboHits();
// Longer maps are worth more
double lengthBonus = double lengthBonus =
0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) + 0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) +
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0); (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
// Longer maps are worth more
value *= lengthBonus; value *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
value *= Math.Pow(0.97, misses); value *= Math.Pow(0.97, misses);
// Combo scaling // Combo scaling
@ -80,17 +76,17 @@ namespace osu.Game.Rulesets.Catch.Difficulty
} }
if (mods.Any(m => m is ModFlashlight)) if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
value *= 1.35 * lengthBonus; value *= 1.35 * lengthBonus;
// Scale the aim value with accuracy _slightly_
value *= Math.Pow(accuracy(), 5.5); value *= Math.Pow(accuracy(), 5.5);
// Custom multipliers for NoFail. SpunOut is not applicable.
if (mods.Any(m => m is ModNoFail)) if (mods.Any(m => m is ModNoFail))
value *= 0.90; value *= 0.90;
return value; return new CatchPerformanceAttributes
{
Total = value
};
} }
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1); private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);

View File

@ -52,16 +52,25 @@ namespace osu.Game.Rulesets.Catch.Edit
return true; return true;
} }
public override bool HandleFlip(Direction direction) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{ {
if (SelectedItems.Count == 0)
return false;
// This could be implemented in the future if there's a requirement for it.
if (direction == Direction.Vertical)
return false;
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems); var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
bool changed = false; bool changed = false;
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (h is CatchHitObject catchObject) if (h is CatchHitObject catchObject)
changed |= handleFlip(selectionRange, catchObject); changed |= handleFlip(selectionRange, catchObject, flipOverOrigin);
}); });
return changed; return changed;
} }
@ -116,7 +125,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return Math.Clamp(deltaX, lowerBound, upperBound); return Math.Clamp(deltaX, lowerBound, upperBound);
} }
private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject) private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject, bool flipOverOrigin)
{ {
switch (hitObject) switch (hitObject)
{ {
@ -124,7 +133,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return false; return false;
case JuiceStream juiceStream: case JuiceStream juiceStream:
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX); juiceStream.OriginalX = getFlippedPosition(juiceStream.OriginalX);
foreach (var point in juiceStream.Path.ControlPoints) foreach (var point in juiceStream.Path.ControlPoints)
point.Position *= new Vector2(-1, 1); point.Position *= new Vector2(-1, 1);
@ -133,9 +142,11 @@ namespace osu.Game.Rulesets.Catch.Edit
return true; return true;
default: default:
hitObject.OriginalX = selectionRange.GetFlippedPosition(hitObject.OriginalX); hitObject.OriginalX = getFlippedPosition(hitObject.OriginalX);
return true; return true;
} }
float getFlippedPosition(float original) => flipOverOrigin ? CatchPlayfield.WIDTH - original : selectionRange.GetFlippedPosition(original);
} }
} }
} }

View File

@ -90,9 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2); .FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45 const float angle_variance = 15; // should be less than 45
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4); directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variance, angle_variance, randomSeed, 4);
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5); directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variance, angle_variance, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
} }

View File

@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Android.App; using Android.App;
using Android.Content.PM;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Tests; using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.Android namespace osu.Game.Rulesets.Mania.Tests.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
public class MainActivity : AndroidGameActivity public class MainActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuTestBrowser(); protected override Framework.Game CreateGame() => new OsuTestBrowser();

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -24,9 +25,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache) private void load()
{ {
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction); config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
} }
} }

View File

@ -29,7 +29,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo(); private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())); private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())
{
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo
}
});
private readonly ManiaBeatSnapGrid beatSnapGrid; private readonly ManiaBeatSnapGrid beatSnapGrid;

View File

@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
AddStep("setup compose screen", () => AddStep("setup compose screen", () =>
{ {
var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
{ {
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
}; });
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);

View File

@ -203,10 +203,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
{ {
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
}, }),
Composer = new ManiaHitObjectComposer(new ManiaRuleset()) Composer = new ManiaHitObjectComposer(new ManiaRuleset())
}; };

View File

@ -14,7 +14,6 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue> public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";

View File

@ -4,11 +4,14 @@ Version: 2.5
[Mania] [Mania]
Keys: 4 Keys: 4
ColumnLineWidth: 3,1,3,1,1 ColumnLineWidth: 3,1,3,1,1
Hit0: mania/hit0 // some skins found in the wild had configuration keys where the @2x suffix was included in the values.
Hit50: mania/hit50 // the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything
Hit100: mania/hit100 // if @2x assets are present.
Hit200: mania/hit200 Hit0: mania/hit0@2x
Hit300: mania/hit300 Hit50: mania/hit50@2x
Hit300g: mania/hit300g Hit100: mania/hit100@2x
Hit200: mania/hit200@2x
Hit300: mania/hit300@2x
Hit300g: mania/hit300g@2x
StageLeft: mania/stage-left StageLeft: mania/stage-left
StageRight: mania/stage-right StageRight: mania/stage-right

View File

@ -5,8 +5,10 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -23,15 +25,24 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
if (hitWindows.IsHitResultAllowed(result)) if (hitWindows.IsHitResultAllowed(result))
{ {
AddStep("Show " + result.GetDescription(), () => SetContents(_ => AddStep("Show " + result.GetDescription(), () =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) {
{ SetContents(_ =>
Type = result new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
}, null) {
{ Type = result
Anchor = Anchor.Centre, }, null)
Origin = Anchor.Centre, {
})); Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
// (see `LegacyManiaJudgementPiece.load()`).
// this prevents the judgements showing somewhere below or above the bounding box of the judgement.
foreach (var legacyPiece in this.ChildrenOfType<LegacyManiaJudgementPiece>())
legacyPiece.Y = 0;
});
} }
} }
} }

View File

@ -24,13 +24,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
public TestSceneHitExplosion() public TestSceneHitExplosion()
{ {
int runcount = 0; int runCount = 0;
AddRepeatStep("explode", () => AddRepeatStep("explode", () =>
{ {
runcount++; runCount++;
if (runcount % 15 > 12) if (runCount % 15 > 12)
return; return;
int poolIndex = 0; int poolIndex = 0;
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
c.Add(hitExplosionPools[poolIndex].Get(e => c.Add(hitExplosionPools[poolIndex].Get(e =>
{ {
e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement())); e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
e.Anchor = Anchor.Centre; e.Anchor = Anchor.Centre;
e.Origin = Anchor.Centre; e.Origin = Anchor.Centre;

View File

@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}, },
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
SliderTickRate = 4, SliderTickRate = 4,
OverallDifficulty = 10, OverallDifficulty = 10,
@ -306,7 +306,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}, },
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = tick_rate }, Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
Ruleset = new ManiaRuleset().RulesetInfo Ruleset = new ManiaRuleset().RulesetInfo
}, },
}; };
@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}, },
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo Ruleset = new ManiaRuleset().RulesetInfo
}, },
}; };

View File

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class TestSceneTimingBasedNoteColouring : OsuTestScene public class TestSceneTimingBasedNoteColouring : OsuTestScene
{ {
[Resolved]
private RulesetConfigCache configCache { get; set; }
private Bindable<bool> configTimingBasedNoteColouring; private Bindable<bool> configTimingBasedNoteColouring;
private ManualClock clock; private ManualClock clock;
@ -48,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests
}); });
AddStep("retrieve config bindable", () => AddStep("retrieve config bindable", () =>
{ {
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
configTimingBasedNoteColouring = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring); configTimingBasedNoteColouring = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring);
}); });
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public override IEnumerable<BeatmapStatistic> GetStatistics() public override IEnumerable<BeatmapStatistic> GetStatistics()
{ {
int notes = HitObjects.Count(s => s is Note); int notes = HitObjects.Count(s => s is Note);
int holdnotes = HitObjects.Count(s => s is HoldNote); int holdNotes = HitObjects.Count(s => s is HoldNote);
return new[] return new[]
{ {
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{ {
Name = @"Hold Note Count", Name = @"Hold Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdnotes.ToString(), Content = holdNotes.ToString(),
}, },
}; };
} }

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
{ {
double roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize); double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize);
return (int)Math.Max(1, roundedCircleSize); return (int)Math.Max(1, roundedCircleSize);
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
yield return v; yield return v;
// Todo: osu!mania doesn't output MaxCombo attribute for some reason. // Todo: osu!mania doesn't output MaxCombo attribute for some reason.
yield return (ATTRIB_ID_STRAIN, StarRating); yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier); yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
} }
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values);
StarRating = values[ATTRIB_ID_STRAIN]; StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER]; ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
} }

View File

@ -0,0 +1,20 @@
// 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 Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceAttributes : PerformanceAttributes
{
[JsonProperty("difficulty")]
public double Difficulty { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("scaled_score")]
public double ScaledScore { get; set; }
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
} }
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) public override PerformanceAttributes Calculate()
{ {
mods = Score.Mods; mods = Score.Mods;
scaledScore = Score.TotalScore; scaledScore = Score.TotalScore;
@ -61,48 +61,46 @@ namespace osu.Game.Rulesets.Mania.Difficulty
if (mods.Any(m => m is ModEasy)) if (mods.Any(m => m is ModEasy))
multiplier *= 0.5; multiplier *= 0.5;
double strainValue = computeStrainValue(); double difficultyValue = computeDifficultyValue();
double accValue = computeAccuracyValue(strainValue); double accValue = computeAccuracyValue(difficultyValue);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(strainValue, 1.1) + Math.Pow(difficultyValue, 1.1) +
Math.Pow(accValue, 1.1), 1.0 / 1.1 Math.Pow(accValue, 1.1), 1.0 / 1.1
) * multiplier; ) * multiplier;
if (categoryDifficulty != null) return new ManiaPerformanceAttributes
{ {
categoryDifficulty["Strain"] = strainValue; Difficulty = difficultyValue,
categoryDifficulty["Accuracy"] = accValue; Accuracy = accValue,
} ScaledScore = scaledScore,
Total = totalValue
return totalValue; };
} }
private double computeStrainValue() private double computeDifficultyValue()
{ {
// Obtain strain difficulty double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
double strainValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
// Longer maps are worth more difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
if (scaledScore <= 500000) if (scaledScore <= 500000)
strainValue = 0; difficultyValue = 0;
else if (scaledScore <= 600000) else if (scaledScore <= 600000)
strainValue *= (scaledScore - 500000) / 100000 * 0.3; difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
else if (scaledScore <= 700000) else if (scaledScore <= 700000)
strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
else if (scaledScore <= 800000) else if (scaledScore <= 800000)
strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
else if (scaledScore <= 900000) else if (scaledScore <= 900000)
strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
else else
strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
return strainValue; return difficultyValue;
} }
private double computeAccuracyValue(double strainValue) private double computeAccuracyValue(double difficultyValue)
{ {
if (Attributes.GreatHitWindow <= 0) if (Attributes.GreatHitWindow <= 0)
return 0; return 0;
@ -110,12 +108,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
* strainValue * difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
// accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
return accuracyValue; return accuracyValue;
} }

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5); public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime, 0.5);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);

View File

@ -0,0 +1,23 @@
// 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.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHealthProcessor : DrainingHealthProcessor
{
/// <inheritdoc/>
public ManiaHealthProcessor(double drainStartTime, double drainLenience = 0)
: base(drainStartTime, drainLenience)
{
}
protected override HitResult GetSimulatedHitResult(Judgement judgement)
{
// Users are not expected to attain perfect judgements for all notes due to the tighter hit window.
return judgement.MaxResult == HitResult.Perfect ? HitResult.Great : judgement.MaxResult;
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
/// Mapping of <see cref="HitResult"/> to their corresponding /// Mapping of <see cref="HitResult"/> to their corresponding
/// <see cref="LegacyManiaSkinConfigurationLookups"/> value. /// <see cref="LegacyManiaSkinConfigurationLookups"/> value.
/// </summary> /// </summary>
private static readonly IReadOnlyDictionary<HitResult, LegacyManiaSkinConfigurationLookups> hitresult_mapping private static readonly IReadOnlyDictionary<HitResult, LegacyManiaSkinConfigurationLookups> hit_result_mapping
= new Dictionary<HitResult, LegacyManiaSkinConfigurationLookups> = new Dictionary<HitResult, LegacyManiaSkinConfigurationLookups>
{ {
{ HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g }, { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g },
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
/// Mapping of <see cref="HitResult"/> to their corresponding /// Mapping of <see cref="HitResult"/> to their corresponding
/// default filenames. /// default filenames.
/// </summary> /// </summary>
private static readonly IReadOnlyDictionary<HitResult, string> default_hitresult_skin_filenames private static readonly IReadOnlyDictionary<HitResult, string> default_hit_result_skin_filenames
= new Dictionary<HitResult, string> = new Dictionary<HitResult, string>
{ {
{ HitResult.Perfect, "mania-hit300g" }, { HitResult.Perfect, "mania-hit300g" },
@ -126,11 +126,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)
{ {
if (!hitresult_mapping.ContainsKey(result)) if (!hit_result_mapping.ContainsKey(result))
return null; return null;
string filename = this.GetManiaSkinConfig<string>(hitresult_mapping[result])?.Value string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value
?? default_hitresult_skin_filenames[result]; ?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true); var animation = this.GetAnimation(filename, true, true);
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
const float angle_variangle = 15; // should be less than 45 const float angle_variance = 15; // should be less than 45
const float roundness = 80; const float roundness = 80;
const float initial_height = 10; const float initial_height = 10;
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI
Masking = true, Masking = true,
Size = new Vector2(0.01f, initial_height), Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), Rotation = RNG.NextSingle(-angle_variance, angle_variance),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.UI
Masking = true, Masking = true,
Size = new Vector2(0.01f, initial_height), Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), Rotation = RNG.NextSingle(-angle_variance, angle_variance),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h); public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h);
public void Add(BarLine barline) => stages.ForEach(s => s.Add(barline)); public void Add(BarLine barLine) => stages.ForEach(s => s.Add(barLine));
/// <summary> /// <summary>
/// Retrieves a column from a screen-space position. /// Retrieves a column from a screen-space position.

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h); public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline)); public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine));
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {

View File

@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Android.App; using Android.App;
using Android.Content.PM;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Tests; using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.Android namespace osu.Game.Rulesets.Osu.Tests.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
public class MainActivity : AndroidGameActivity public class MainActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuTestBrowser(); protected override Framework.Game CreateGame() => new OsuTestBrowser();

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
var beatmap = new Beatmap<HitObject> var beatmap = new Beatmap<HitObject>
{ {
HitObjects = hitObjects, HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) } BeatmapInfo = new BeatmapInfo { Difficulty = new BeatmapDifficulty(beatmapDifficulty) }
}; };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));

View File

@ -40,7 +40,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public TestSceneOsuDistanceSnapGrid() public TestSceneOsuDistanceSnapGrid()
{ {
editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap = new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo
}
});
} }
[SetUp] [SetUp]

View File

@ -1,7 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -47,6 +50,126 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddBlueprint(new TestSliderBlueprint(slider), drawableObject); AddBlueprint(new TestSliderBlueprint(slider), drawableObject);
}); });
[Test]
public void TestSelection()
{
moveMouseToControlPoint(0);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
assertSelectionCount(1);
assertSelected(0);
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
assertSelectionCount(1);
assertSelected(0);
moveMouseToControlPoint(3);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
assertSelectionCount(1);
assertSelected(3);
AddStep("press control", () => InputManager.PressKey(Key.ControlLeft));
moveMouseToControlPoint(2);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
assertSelectionCount(2);
assertSelected(2);
assertSelected(3);
moveMouseToControlPoint(0);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
assertSelectionCount(3);
assertSelected(0);
assertSelected(2);
assertSelected(3);
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
assertSelectionCount(3);
assertSelected(0);
assertSelected(2);
assertSelected(3);
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
assertSelectionCount(1);
assertSelected(0);
moveMouseToRelativePosition(new Vector2(350, 0));
AddStep("ctrl+click to create new point", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressButton(MouseButton.Left);
});
assertSelectionCount(1);
assertSelected(3);
AddStep("release ctrl+click", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
assertSelectionCount(1);
assertSelected(3);
}
[Test]
public void TestNewControlPointCreation()
{
moveMouseToRelativePosition(new Vector2(350, 0));
AddStep("ctrl+click to create new point", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressButton(MouseButton.Left);
});
AddAssert("slider has 6 control points", () => slider.Path.ControlPoints.Count == 6);
AddStep("release ctrl+click", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
// ensure that the next drag doesn't attempt to move the placement that just finished.
moveMouseToRelativePosition(new Vector2(0, 50));
AddStep("press left mouse", () => InputManager.PressButton(MouseButton.Left));
moveMouseToRelativePosition(new Vector2(0, 100));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(3, new Vector2(350, 0));
moveMouseToRelativePosition(new Vector2(400, 75));
AddStep("ctrl+click to create new point", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressButton(MouseButton.Left);
});
AddAssert("slider has 7 control points", () => slider.Path.ControlPoints.Count == 7);
moveMouseToRelativePosition(new Vector2(350, 75));
AddStep("release ctrl+click", () =>
{
InputManager.ReleaseButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
assertControlPointPosition(5, new Vector2(350, 75));
// ensure that the next drag doesn't attempt to move the placement that just finished.
moveMouseToRelativePosition(new Vector2(0, 50));
AddStep("press left mouse", () => InputManager.PressButton(MouseButton.Left));
moveMouseToRelativePosition(new Vector2(0, 100));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
assertControlPointPosition(5, new Vector2(350, 75));
}
private void assertSelectionCount(int count) =>
AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == count);
private void assertSelected(int index) =>
AddAssert($"{(index + 1).ToOrdinalWords()} control point piece selected",
() => this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value);
private void moveMouseToRelativePosition(Vector2 relativePosition) =>
AddStep($"move mouse to {relativePosition}", () =>
{
Vector2 position = slider.Position + relativePosition;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
[Test] [Test]
public void TestDragControlPoint() public void TestDragControlPoint()
{ {
@ -60,6 +183,83 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointType(0, PathType.PerfectCurve); assertControlPointType(0, PathType.PerfectCurve);
} }
[Test]
public void TestDragMultipleControlPoints()
{
moveMouseToControlPoint(2);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.LControl));
moveMouseToControlPoint(3);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(4);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(2);
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
addMovementStep(new Vector2(450, 50));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
assertControlPointPosition(2, new Vector2(450, 50));
assertControlPointType(2, PathType.PerfectCurve);
assertControlPointPosition(3, new Vector2(550, 50));
assertControlPointPosition(4, new Vector2(550, 200));
AddStep("release control", () => InputManager.ReleaseKey(Key.LControl));
}
[Test]
public void TestDragMultipleControlPointsIncludingHead()
{
moveMouseToControlPoint(0);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.LControl));
moveMouseToControlPoint(3);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(4);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
moveMouseToControlPoint(3);
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
addMovementStep(new Vector2(550, 50));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
// note: if the head is part of the selection being moved, the entire slider is moved.
// the unselected nodes will therefore change position relative to the slider head.
AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50)));
assertControlPointPosition(0, Vector2.Zero);
assertControlPointType(0, PathType.PerfectCurve);
assertControlPointPosition(1, new Vector2(0, 100));
assertControlPointPosition(2, new Vector2(150, -50));
assertControlPointPosition(3, new Vector2(400, 0));
assertControlPointPosition(4, new Vector2(400, 150));
AddStep("release control", () => InputManager.ReleaseKey(Key.LControl));
}
[Test] [Test]
public void TestDragControlPointAlmostLinearlyExterior() public void TestDragControlPointAlmostLinearlyExterior()
{ {

View File

@ -0,0 +1,225 @@
// 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.Input.Events;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderSnapping : EditorTestScene
{
private const double beat_length = 1000;
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
return new TestBeatmap(ruleset, false)
{
ControlPointInfo = controlPointInfo
};
}
private Slider slider;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add unsnapped slider", () => EditorBeatmap.Add(slider = new Slider
{
StartTime = 0,
Position = OsuPlayfield.BASE_SIZE / 5,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(Vector2.Zero),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5),
new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5)
}
}
}));
AddStep("set beat divisor to 1/1", () =>
{
var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
beatDivisor.Value = 1;
});
}
[Test]
public void TestMovingUnsnappedSliderNodesSnaps()
{
PathControlPointPiece sliderEnd = null;
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select slider end", () =>
{
sliderEnd = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last());
InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre);
});
AddStep("move slider end", () =>
{
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre - new Vector2(0, 20));
InputManager.ReleaseButton(MouseButton.Left);
});
assertSliderSnapped(true);
}
[Test]
public void TestAddingControlPointToUnsnappedSliderNodesSnaps()
{
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to new point location", () =>
{
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2);
});
AddStep("move slider end", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
assertSliderSnapped(true);
}
[Test]
public void TestRemovingControlPointFromUnsnappedSliderNodesSnaps()
{
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to second control point", () =>
{
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
InputManager.MoveMouseTo(secondPiece);
});
AddStep("quick delete", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.PressButton(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
assertSliderSnapped(true);
}
[Test]
public void TestResizingUnsnappedSliderSnaps()
{
SelectionBoxScaleHandle handle = null;
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to scale handle", () =>
{
handle = this.ChildrenOfType<SelectionBoxScaleHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
});
AddStep("scale slider", () =>
{
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre + new Vector2(20, 20));
InputManager.ReleaseButton(MouseButton.Left);
});
assertSliderSnapped(true);
}
[Test]
public void TestRotatingUnsnappedSliderDoesNotSnap()
{
SelectionBoxRotationHandle handle = null;
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to rotate handle", () =>
{
handle = this.ChildrenOfType<SelectionBoxRotationHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
});
AddStep("scale slider", () =>
{
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre + new Vector2(0, 20));
InputManager.ReleaseButton(MouseButton.Left);
});
assertSliderSnapped(false);
}
[Test]
public void TestFlippingSliderDoesNotSnap()
{
OsuSelectionHandler selectionHandler = null;
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("flip slider horizontally", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipHorizontally));
});
assertSliderSnapped(false);
AddStep("flip slider vertically", () =>
{
selectionHandler = this.ChildrenOfType<OsuSelectionHandler>().Single();
selectionHandler.OnPressed(new KeyBindingPressEvent<GlobalAction>(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
});
assertSliderSnapped(false);
}
[Test]
public void TestReversingSliderDoesNotSnap()
{
assertSliderSnapped(false);
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("reverse slider", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.ControlLeft);
});
assertSliderSnapped(false);
}
private void assertSliderSnapped(bool snapped)
=> AddAssert($"slider is {(snapped ? "" : "not ")}snapped", () =>
{
double durationInBeatLengths = slider.Duration / beat_length;
double fractionalPart = durationInBeatLengths - (int)durationInBeatLengths;
return Precision.AlmostEquals(fractionalPart, 0) == snapped;
});
}
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
CircleSize = 8 CircleSize = 8
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, Difficulty = new BeatmapDifficulty { CircleSize = 6 },
Ruleset = ruleset Ruleset = ruleset
} }
}; };

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty { OverallDifficulty = 10 }, Difficulty = new BeatmapDifficulty { OverallDifficulty = 10 },
Ruleset = ruleset Ruleset = ruleset
} }
}; };

View File

@ -358,7 +358,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}, },
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo
}, },
}); });

View File

@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -46,9 +47,9 @@ namespace osu.Game.Rulesets.Osu.Tests
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache) private void load()
{ {
var config = (OsuRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); var config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn); config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
} }

View File

@ -364,7 +364,7 @@ namespace osu.Game.Rulesets.Osu.Tests
HitObjects = hitObjects, HitObjects = hitObjects,
BeatmapInfo = BeatmapInfo =
{ {
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Difficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo
}, },
}); });

View File

@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuDifficultyAttributes : DifficultyAttributes public class OsuDifficultyAttributes : DifficultyAttributes
{ {
[JsonProperty("aim_strain")] [JsonProperty("aim_difficulty")]
public double AimStrain { get; set; } public double AimDifficulty { get; set; }
[JsonProperty("speed_strain")] [JsonProperty("speed_difficulty")]
public double SpeedStrain { get; set; } public double SpeedDifficulty { get; set; }
[JsonProperty("flashlight_rating")] [JsonProperty("flashlight_difficulty")]
public double FlashlightRating { get; set; } public double FlashlightDifficulty { get; set; }
[JsonProperty("slider_factor")] [JsonProperty("slider_factor")]
public double SliderFactor { get; set; } public double SliderFactor { get; set; }
@ -43,15 +43,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
foreach (var v in base.ToDatabaseAttributes()) foreach (var v in base.ToDatabaseAttributes())
yield return v; yield return v;
yield return (ATTRIB_ID_AIM, AimStrain); yield return (ATTRIB_ID_AIM, AimDifficulty);
yield return (ATTRIB_ID_SPEED, SpeedStrain); yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty); yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_STRAIN, StarRating); yield return (ATTRIB_ID_DIFFICULTY, StarRating);
if (ShouldSerializeFlashlightRating()) if (ShouldSerializeFlashlightRating())
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightRating); yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
} }
@ -60,18 +60,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values);
AimStrain = values[ATTRIB_ID_AIM]; AimDifficulty = values[ATTRIB_ID_AIM];
SpeedStrain = values[ATTRIB_ID_SPEED]; SpeedDifficulty = values[ATTRIB_ID_SPEED];
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY]; OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_STRAIN]; StarRating = values[ATTRIB_ID_DIFFICULTY];
FlashlightRating = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
} }
// Used implicitly by Newtonsoft.Json to not serialize flashlight property in some cases. #region Newtonsoft.Json implicit ShouldSerialize() methods
// The properties in this region are used implicitly by Newtonsoft.Json to not serialise certain fields in some cases.
// They rely on being named exactly the same as the corresponding fields (casing included) and as such should NOT be renamed
// unless the fields are also renamed.
[UsedImplicitly] [UsedImplicitly]
public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight); public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight);
#endregion
} }
} }

View File

@ -74,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
StarRating = starRating, StarRating = starRating,
Mods = mods, Mods = mods,
AimStrain = aimRating, AimDifficulty = aimRating,
SpeedStrain = speedRating, SpeedDifficulty = speedRating,
FlashlightRating = flashlightRating, FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor, SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,

View File

@ -0,0 +1,26 @@
// 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 Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceAttributes : PerformanceAttributes
{
[JsonProperty("aim")]
public double Aim { get; set; }
[JsonProperty("speed")]
public double Speed { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("flashlight")]
public double Flashlight { get; set; }
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
}
}

View File

@ -25,14 +25,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
private int effectiveMissCount; private double effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score) : base(ruleset, attributes, score)
{ {
} }
public override double Calculate(Dictionary<string, double> categoryRatings = null) public override PerformanceAttributes Calculate()
{ {
mods = Score.Mods; mods = Score.Mods;
accuracy = Score.Accuracy; accuracy = Score.Accuracy;
@ -45,11 +45,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail)) if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut)) if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax)) if (mods.Any(h => h is OsuModRelax))
@ -72,42 +71,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1 Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
) * multiplier; ) * multiplier;
if (categoryRatings != null) return new OsuPerformanceAttributes
{ {
categoryRatings.Add("Aim", aimValue); Aim = aimValue,
categoryRatings.Add("Speed", speedValue); Speed = speedValue,
categoryRatings.Add("Accuracy", accuracyValue); Accuracy = accuracyValue,
categoryRatings.Add("Flashlight", flashlightValue); Flashlight = flashlightValue,
categoryRatings.Add("OD", Attributes.OverallDifficulty); EffectiveMissCount = effectiveMissCount,
categoryRatings.Add("AR", Attributes.ApproachRate); Total = totalValue
categoryRatings.Add("Max Combo", Attributes.MaxCombo); };
}
return totalValue;
} }
private double computeAimValue() private double computeAimValue()
{ {
double rawAim = Attributes.AimStrain; double rawAim = Attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8); rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus; aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount); aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling. aimValue *= getComboScalingFactor();
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (Attributes.ApproachRate > 10.33)
@ -136,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
} }
aimValue *= accuracy; aimValue *= accuracy;
// It is important to also consider accuracy difficulty when doing that. // It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue; return aimValue;
@ -144,20 +136,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue() private double computeSpeedValue()
{ {
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus; speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling. speedValue *= getComboScalingFactor();
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (Attributes.ApproachRate > 10.33)
@ -227,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!mods.Any(h => h is OsuModFlashlight)) if (!mods.Any(h => h is OsuModFlashlight))
return 0.0; return 0.0;
double rawFlashlight = Attributes.FlashlightRating; double rawFlashlight = Attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8); rawFlashlight = Math.Pow(rawFlashlight, 0.8);
@ -236,11 +225,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling. flashlightValue *= getComboScalingFactor();
if (Attributes.MaxCombo > 0)
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -254,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue; return flashlightValue;
} }
private int calculateEffectiveMissCount() private double calculateEffectiveMissCount()
{ {
// Guess the number of misses + slider breaks from combo // Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0; double comboBasedMissCount = 0.0;
@ -266,12 +253,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
} }
// Clamp misscount since it's derived from combo and can be higher than total hits and that breaks some calculations // Clamp miss count since it's derived from combo and can be higher than total hits and that breaks some calculations
comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits); comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount)); return Math.Max(countMiss, comboBasedMissCount);
} }
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }

View File

@ -16,11 +16,9 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -33,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
{ {
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection; public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public Action<PathControlPoint> DragStarted;
public Action<DragEvent> DragInProgress;
public Action DragEnded;
public List<PathControlPoint> PointsInSegment; public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
@ -42,12 +45,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Container marker; private readonly Container marker;
private readonly Drawable markerRing; private readonly Drawable markerRing;
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -138,6 +135,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
updateMarkerDisplay(); updateMarkerDisplay();
} }
// Used to pair up mouse down/drag events with their corresponding mouse up events,
// to avoid deselecting the piece by accident when the mouse up corresponding to the mouse down/drag fires.
private bool keepSelection;
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (RequestSelection == null) if (RequestSelection == null)
@ -146,22 +147,41 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Button) switch (e.Button)
{ {
case MouseButton.Left: case MouseButton.Left:
// if control is pressed, do not do anything as the user may be adding to current selection
// or dragging all currently selected control points.
// if it isn't and the user's intent is to deselect, deselection will happen on mouse up.
if (e.ControlPressed && IsSelected.Value)
return true;
RequestSelection.Invoke(this, e); RequestSelection.Invoke(this, e);
keepSelection = true;
return true; return true;
case MouseButton.Right: case MouseButton.Right:
if (!IsSelected.Value) if (!IsSelected.Value)
RequestSelection.Invoke(this, e); RequestSelection.Invoke(this, e);
keepSelection = true;
return false; // Allow context menu to show return false; // Allow context menu to show
} }
return false; return false;
} }
protected override bool OnClick(ClickEvent e) => RequestSelection != null; protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
private Vector2 dragStartPosition; // ctrl+click deselects this piece, but only if this event
private PathType? dragPathType; // wasn't immediately preceded by a matching mouse down or drag.
if (IsSelected.Value && e.ControlPressed && !keepSelection)
IsSelected.Value = false;
keepSelection = false;
}
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
@ -170,54 +190,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left) if (e.Button == MouseButton.Left)
{ {
dragStartPosition = ControlPoint.Position; DragStarted?.Invoke(ControlPoint);
dragPathType = PointsInSegment[0].Type; keepSelection = true;
changeHandler?.BeginChange();
return true; return true;
} }
return false; return false;
} }
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e) => DragInProgress?.Invoke(e);
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
double oldStartTime = slider.StartTime;
if (ControlPoint == slider.Path.ControlPoints[0]) protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position;
slider.Position += movementDelta;
slider.StartTime = result?.Time ?? slider.StartTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position -= movementDelta;
}
else
ControlPoint.Position = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
if (!slider.Path.HasValidLength)
{
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
return;
}
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
PointsInSegment[0].Type = dragPathType;
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint); private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
@ -267,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (pathType) switch (pathType)
{ {
case PathType.Catmull: case PathType.Catmull:
return colours.Seafoam; return colours.SeaFoam;
case PathType.Bezier: case PathType.Bezier:
return colours.Pink; return colours.Pink;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -16,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -40,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> RemoveControlPointsRequested; public Action<List<PathControlPoint>> RemoveControlPointsRequested;
[Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection) public PathControlPointVisualiser(Slider slider, bool allowSelection)
{ {
this.slider = slider; this.slider = slider;
@ -64,6 +69,39 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(slider.Path.ControlPoints); controlPoints.BindTo(slider.Path.ControlPoints);
} }
/// <summary>
/// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece"/>s.
/// </summary>
public void SetSelectionTo(PathControlPoint pathControlPoint)
{
foreach (var p in Pieces)
p.IsSelected.Value = p.ControlPoint == pathControlPoint;
}
/// <summary>
/// Delete all visually selected <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns></returns>
public bool DeleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
switch (e.Action) switch (e.Action)
@ -87,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Pieces.Add(new PathControlPointPiece(slider, point).With(d => Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
{ {
if (allowSelection) if (allowSelection)
d.RequestSelection = selectPiece; d.RequestSelection = selectionRequested;
d.DragStarted = dragStarted;
d.DragInProgress = dragInProgress;
d.DragEnded = dragEnded;
})); }));
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
@ -119,6 +161,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (Pieces.Any(piece => piece.IsHovered))
return false;
foreach (var piece in Pieces) foreach (var piece in Pieces)
{ {
piece.IsSelected.Value = false; piece.IsSelected.Value = false;
@ -142,15 +187,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
} }
private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e)
{ {
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
piece.IsSelected.Toggle(); piece.IsSelected.Toggle();
else else
{ SetSelectionTo(piece.ControlPoint);
foreach (var p in Pieces)
p.IsSelected.Value = p == piece;
}
} }
/// <summary> /// <summary>
@ -184,25 +226,87 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
public bool DeleteSelected() #region Drag handling
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be deleted private Vector2[] dragStartPositions;
if (toRemove.Count == 0) private PathType?[] dragPathTypes;
return false; private int draggedControlPointIndex;
private HashSet<PathControlPoint> selectedControlPoints;
private void dragStarted(PathControlPoint controlPoint)
{
dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray();
dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray();
draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint);
selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint));
Debug.Assert(draggedControlPointIndex >= 0);
changeHandler?.BeginChange(); changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
} }
private void dragInProgress(DragEvent e)
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position;
double oldStartTime = slider.StartTime;
if (selectedControlPoints.Contains(slider.Path.ControlPoints[0]))
{
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;
slider.Position += movementDelta;
slider.StartTime = result?.Time ?? slider.StartTime;
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
{
var controlPoint = slider.Path.ControlPoints[i];
// Since control points are relative to the position of the slider, all points that are _not_ selected
// need to be offset _back_ by the delta corresponding to the movement of the head point.
// All other selected control points (if any) will move together with the head point
// (and so they will not move at all, relative to each other).
if (!selectedControlPoints.Contains(controlPoint))
controlPoint.Position -= movementDelta;
}
}
else
{
for (int i = 0; i < controlPoints.Count; ++i)
{
var controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
controlPoint.Position = dragStartPositions[i] + (e.MousePosition - e.MouseDownPosition);
}
}
// Snap the path to the current beat divisor before checking length validity.
slider.SnapTo(snapProvider);
if (!slider.Path.HasValidLength)
{
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
// Snap the path length again to undo the invalid length.
slider.SnapTo(snapProvider);
return;
}
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Type = dragPathTypes[i];
}
private void dragEnded() => changeHandler?.EndChange();
#endregion
public MenuItem[] ContextMenuItems public MenuItem[] ContextMenuItems
{ {
get get

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -81,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.BindTo(HitObject.Path.ControlPoints); controlPoints.BindTo(HitObject.Path.ControlPoints);
pathVersion.BindTo(HitObject.Path.Version); pathVersion.BindTo(HitObject.Path.Version);
pathVersion.BindValueChanged(_ => updatePath()); pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
BodyPiece.UpdateFrom(HitObject); BodyPiece.UpdateFrom(HitObject);
} }
@ -140,7 +139,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case MouseButton.Left: case MouseButton.Left:
if (e.ControlPressed && IsSelected) if (e.ControlPressed && IsSelected)
{ {
placementControlPointIndex = addControlPoint(e.MousePosition); changeHandler?.BeginChange();
placementControlPoint = addControlPoint(e.MousePosition);
ControlPointVisualiser?.SetSelectionTo(placementControlPoint);
return true; // Stop input from being handled and modifying the selection return true; // Stop input from being handled and modifying the selection
} }
@ -150,31 +151,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false; return false;
} }
private int? placementControlPointIndex; [CanBeNull]
private PathControlPoint placementControlPoint;
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e) => placementControlPoint != null;
{
if (placementControlPointIndex != null)
{
changeHandler?.BeginChange();
return true;
}
return false;
}
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
Debug.Assert(placementControlPointIndex != null); if (placementControlPoint != null)
placementControlPoint.Position = e.MousePosition - HitObject.Position;
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position = e.MousePosition - HitObject.Position;
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
if (placementControlPointIndex != null) if (placementControlPoint != null)
{ {
placementControlPointIndex = null; placementControlPoint = null;
changeHandler?.EndChange(); changeHandler?.EndChange();
} }
} }
@ -193,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return false; return false;
} }
private int addControlPoint(Vector2 position) private PathControlPoint addControlPoint(Vector2 position)
{ {
position -= HitObject.Position; position -= HitObject.Position;
@ -211,10 +203,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
} }
// Move the control points from the insertion index onwards to make room for the insertion var pathControlPoint = new PathControlPoint { Position = position };
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = position });
return insertionIndex; // Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
HitObject.SnapTo(composer);
return pathControlPoint;
} }
private void removeControlPoints(List<PathControlPoint> toRemove) private void removeControlPoints(List<PathControlPoint> toRemove)
@ -233,7 +229,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.Remove(c); controlPoints.Remove(c);
} }
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted // Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(composer);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength) if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
{ {
placementHandler?.Delete(HitObject); placementHandler?.Delete(HitObject);
@ -248,12 +247,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Position += first; HitObject.Position += first;
} }
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
private void convertToStream() private void convertToStream()
{ {
if (editorBeatmap == null || changeHandler == null || beatDivisor == null) if (editorBeatmap == null || changeHandler == null || beatDivisor == null)

View File

@ -1,15 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -17,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuSelectionHandler : EditorSelectionHandler public class OsuSelectionHandler : EditorSelectionHandler
{ {
[Resolved(CanBeNull = true)]
private IPositionSnapProvider? positionSnapProvider { get; set; }
/// <summary> /// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation. /// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary> /// </summary>
@ -26,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// During a transform, the initial path types of a single selected slider are stored so they /// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation. /// can be maintained throughout the operation.
/// </summary> /// </summary>
private List<PathType?> referencePathTypes; private List<PathType?>? referencePathTypes;
protected override void OnSelectionChanged() protected override void OnSelectionChanged()
{ {
@ -84,18 +92,28 @@ namespace osu.Game.Rulesets.Osu.Edit
return true; return true;
} }
public override bool HandleFlip(Direction direction) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects); var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
bool didFlip = false;
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ {
h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position); var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
h.Position = flippedPosition;
didFlip = true;
}
if (h is Slider slider) if (h is Slider slider)
{ {
didFlip = true;
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
{ {
point.Position = new Vector2( point.Position = new Vector2(
@ -106,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
return true; return didFlip;
} }
public override bool HandleScale(Vector2 scale, Anchor reference) public override bool HandleScale(Vector2 scale, Anchor reference)
@ -186,6 +204,10 @@ namespace osu.Game.Rulesets.Osu.Edit
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type = referencePathTypes[i]; slider.Path.ControlPoints[i].Type = referencePathTypes[i];
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(positionSnapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
@ -195,6 +217,9 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
point.Position = oldControlPoints.Dequeue(); point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(positionSnapProvider);
} }
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => "Spun Out"; public override string Name => "Spun Out";
public override string Acronym => "SO"; public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunout; public override IconUsage? Icon => OsuIcon.ModSpunOut;
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed."; public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;

View File

@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime } ? new SpinnerTick { StartTime = startTime, Position = Position }
: new SpinnerBonusTick { StartTime = startTime }); : new SpinnerBonusTick { StartTime = startTime, Position = Position });
} }
} }

View File

@ -3,15 +3,13 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class SpinnerBackgroundLayer : SpinnerFill public class SpinnerBackgroundLayer : SpinnerFill
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, DrawableHitObject drawableHitObject) private void load()
{ {
Disc.Alpha = 0; Disc.Alpha = 0;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
#nullable enable
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingDrawable : BeatSyncedContainer
{
private readonly Drawable flashingDrawable;
private const float flash_opacity = 0.3f;
public KiaiFlashingDrawable(Func<Drawable?> creationFunc)
{
AutoSizeAxes = Axes.Both;
Children = new[]
{
(creationFunc.Invoke() ?? Empty()).With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}),
flashingDrawable = (creationFunc.Invoke() ?? Empty()).With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
d.Alpha = 0;
d.Blending = BlendingParameters.Additive;
})
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingDrawable
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -1,61 +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 osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingSprite : BeatSyncedContainer
{
private readonly Sprite mainSprite;
private readonly Sprite flashingSprite;
public Texture Texture
{
set
{
mainSprite.Texture = value;
flashingSprite.Texture = value;
}
}
private const float flash_opacity = 0.3f;
public KiaiFlashingSprite()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
mainSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
flashingSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingSprite
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private GameplayState gameplayState { get; set; } private GameplayState gameplayState { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, OsuColour colours) private void load(ISkinSource skin)
{ {
var texture = skin.GetTexture("star2"); var texture = skin.GetTexture("star2");
var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255); var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -68,13 +69,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
Texture overlayTexture = getTextureWithFallback("overlay");
InternalChildren = new[] InternalChildren = new[]
{ {
hitCircleSprite = new KiaiFlashingSprite hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture })
{ {
Texture = baseTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -82,9 +81,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = hitCircleOverlay = new KiaiFlashingSprite Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d))
{ {
Texture = overlayTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -126,6 +124,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return tex ?? skin.GetTexture($"hitcircle{name}"); return tex ?? skin.GetTexture($"hitcircle{name}");
} }
Drawable getAnimationWithFallback(string name, double frameLength)
{
Drawable animation = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
if (!allowFallback)
return animation;
}
return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
}
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorExpand, CursorExpand,
CursorRotate, CursorRotate,
HitCircleOverlayAboveNumber, HitCircleOverlayAboveNumber,
// ReSharper disable once IdentifierTypo
HitCircleOverlayAboveNumer, // Some old skins will have this typo HitCircleOverlayAboveNumer, // Some old skins will have this typo
SpinnerFrequencyModulate, SpinnerFrequencyModulate,
SpinnerNoBlink SpinnerNoBlink

View File

@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
pointGrid.Content = points; pointGrid.Content = points;
if (score.HitEvents == null || score.HitEvents.Count == 0) if (score.HitEvents.Count == 0)
return; return;
// Todo: This should probably not be done like this. // Todo: This should probably not be done like this.

View File

@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, OsuRulesetConfigManager rulesetConfig) private void load(OsuRulesetConfigManager rulesetConfig)
{ {
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail); rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorTrail, showTrail);
} }

View File

@ -2,13 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Android.App; using Android.App;
using Android.Content.PM;
using osu.Framework.Android; using osu.Framework.Android;
using osu.Game.Tests; using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.Android namespace osu.Game.Rulesets.Taiko.Tests.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
public class MainActivity : AndroidGameActivity public class MainActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuTestBrowser(); protected override Framework.Game CreateGame() => new OsuTestBrowser();

View File

@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } }, HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty(), Difficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Artist = @"Unknown", Artist = @"Unknown",
Title = @"Sample Beatmap", Title = @"Sample Beatmap",
AuthorString = @"peppy", Author = { Username = @"peppy" },
}, },
Ruleset = new TaikoRuleset().RulesetInfo Ruleset = new TaikoRuleset().RulesetInfo
}, },

View File

@ -40,10 +40,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
EditorBeatmap = new EditorBeatmap(new TaikoBeatmap()) EditorBeatmap = new EditorBeatmap(new TaikoBeatmap
{ {
BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo } BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo }
}, }),
new TaikoHitObjectComposer(new TaikoRuleset()) new TaikoHitObjectComposer(new TaikoRuleset())
}; };

View File

@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private BarLine createBarLineAtCurrentTime(bool major = false) private BarLine createBarLineAtCurrentTime(bool major = false)
{ {
var barline = new BarLine var barLine = new BarLine
{ {
Major = major, Major = major,
StartTime = Time.Current + 2000, StartTime = Time.Current + 2000,
@ -92,9 +92,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
var cpi = new ControlPointInfo(); var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint { BeatLength = 500 }); cpi.Add(0, new TimingControlPoint { BeatLength = 500 });
barline.ApplyDefaults(cpi, new BeatmapDifficulty()); barLine.ApplyDefaults(cpi, new BeatmapDifficulty());
return barline; return barLine;
} }
} }
} }

View File

@ -158,12 +158,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } }, HitObjects = new List<HitObject> { new Hit { Type = HitType.Centre } },
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
BaseDifficulty = new BeatmapDifficulty(), Difficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Artist = "Unknown", Artist = "Unknown",
Title = "Sample Beatmap", Title = "Sample Beatmap",
AuthorString = "Craftplacer", Author = { Username = "Craftplacer" },
}, },
Ruleset = new TaikoRuleset().RulesetInfo Ruleset = new TaikoRuleset().RulesetInfo
}, },

View File

@ -12,7 +12,6 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
[TestFixture] [TestFixture]
[Timeout(10000)]
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue> public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";

Some files were not shown because too many files have changed in this diff Show More