1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into classic_drumrolls

This commit is contained in:
Justin 2022-07-24 18:33:04 +00:00 committed by GitHub
commit a4f3a0d201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
874 changed files with 13345 additions and 6376 deletions

View File

@ -1,4 +1,2 @@
# Normalize all the line endings # Normalize all the line endings
32a74f95a5c80a0ed18e693f13a47522099df5c3 32a74f95a5c80a0ed18e693f13a47522099df5c3
# Enabled NRT globally
f8830c6850128456266c82de83273204f8b74ac0

View File

@ -16,3 +16,10 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() 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.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.

View File

@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. --> <!-- Contains required properties for osu!framework projects. -->
<Project> <Project>
<PropertyGroup Label="C#"> <PropertyGroup Label="C#">
<LangVersion>8.0</LangVersion> <LangVersion>9.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -8,20 +8,20 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.570.0) aws-partitions (1.601.0)
aws-sdk-core (3.130.0) aws-sdk-core (3.131.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.55.0) aws-sdk-kms (1.57.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.0) aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0) aws-sigv4 (1.5.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.1.0) claide (1.1.0)
@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6) dotenv (2.7.6)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.92.1) excon (0.92.3)
faraday (1.10.0) faraday (1.10.0)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.3) faraday-multipart (1.0.4)
multipart-post (>= 1.2, < 3) multipart-post (~> 2)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.205.1) fastlane (2.206.2)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -110,9 +110,9 @@ GEM
souyuz (= 0.11.1) souyuz (= 0.11.1)
fastlane-plugin-xamarin (0.6.3) fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.16.0) google-apis-androidpublisher_v3 (0.23.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-core (0.4.2) google-apis-core (0.6.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -121,19 +121,19 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.10.0) google-apis-iamcredentials_v1 (0.12.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-playcustomapp_v1 (0.7.0) google-apis-playcustomapp_v1 (0.9.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-apis-storage_v1 (0.11.0) google-apis-storage_v1 (0.16.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.6, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0) google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1) google-cloud-storage (1.36.2)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
@ -141,7 +141,7 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.1.2) googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@ -149,12 +149,12 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.4) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.1) jmespath (1.6.1)
json (2.6.1) json (2.6.2)
jwt (2.3.0) jwt (2.4.1)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
@ -169,10 +169,10 @@ GEM
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.6.0)
public_suffix (4.0.6) public_suffix (4.0.7)
racc (1.6.0) racc (1.6.0)
rake (13.0.6) rake (13.0.6)
representable (3.1.1) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.16.1) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.8) simctl (1.6.8)
@ -205,11 +205,11 @@ GEM
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.1) unf_ext (0.0.8.2)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
webrick (1.7.0) webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.21.0) xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.616.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.722.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.615.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.722.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. -->

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
namespace osu.Android
{
public class AndroidJoystickSettings : SettingsSubsection
{
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
private readonly AndroidJoystickHandler joystickHandler;
private readonly Bindable<bool> enabled = new BindableBool(true);
private SettingsSlider<float> deadzoneSlider = null!;
private Bindable<float> handlerDeadzone = null!;
private Bindable<float> localDeadzone = null!;
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
{
this.joystickHandler = joystickHandler;
}
[BackgroundDependencyLoader]
private void load()
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
localDeadzone = handlerDeadzone.GetUnboundCopy();
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = enabled
},
deadzoneSlider = new SettingsSlider<float>
{
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
KeyboardStep = 0.01f,
DisplayAsPercentage = true,
Current = localDeadzone,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
enabled.BindTo(joystickHandler.Enabled);
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
handlerDeadzone.BindValueChanged(val =>
{
bool disabled = localDeadzone.Disabled;
localDeadzone.Disabled = false;
localDeadzone.Value = val.NewValue;
localDeadzone.Disabled = disabled;
}, true);
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
}
}
}

View File

@ -0,0 +1,97 @@
// 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.OS;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Android
{
public class AndroidMouseSettings : SettingsSubsection
{
private readonly AndroidMouseHandler mouseHandler;
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
private Bindable<double> handlerSensitivity = null!;
private Bindable<double> localSensitivity = null!;
private Bindable<bool> relativeMode = null!;
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
{
this.mouseHandler = mouseHandler;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
localSensitivity = handlerSensitivity.GetUnboundCopy();
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
// High precision/pointer capture is only available on Android 8.0 and up
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
},
new MouseSettings.SensitivitySetting
{
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity,
},
});
}
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseButtons,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});
}
protected override void LoadComplete()
{
base.LoadComplete();
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
handlerSensitivity.BindValueChanged(val =>
{
bool disabled = localSensitivity.Disabled;
localSensitivity.Disabled = false;
localSensitivity.Value = val.NewValue;
localSensitivity.Disabled = disabled;
}, true);
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
}
}
}

View File

@ -7,7 +7,11 @@ using System;
using Android.App; using Android.App;
using Android.OS; using Android.OS;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
using Xamarin.Essentials; using Xamarin.Essentials;
@ -75,10 +79,31 @@ namespace osu.Android
LoadComponentAsync(new GameplayScreenRotationLocker(), Add); LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
} }
public override void SetHost(GameHost host)
{
base.SetHost(host);
host.Window.CursorState |= CursorState.Hidden;
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh);
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private class AndroidBatteryInfo : BatteryInfo private class AndroidBatteryInfo : BatteryInfo
{ {
public override double ChargeLevel => Battery.ChargeLevel; public override double ChargeLevel => Battery.ChargeLevel;

View File

@ -26,6 +26,8 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AndroidJoystickSettings.cs" />
<Compile Include="AndroidMouseSettings.cs" />
<Compile Include="GameplayScreenRotationLocker.cs" /> <Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" /> <Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" /> <Compile Include="OsuGameAndroid.cs" />

View File

@ -57,7 +57,7 @@ namespace osu.Desktop
client.OnReady += onReady; client.OnReady += onReady;
// safety measure for now, until we performance test / improve backoff for failed connections. // safety measure for now, until we performance test / improve backoff for failed connections.
client.OnConnectionFailed += (_, __) => client.Deinitialize(); client.OnConnectionFailed += (_, _) => client.Deinitialize();
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -20,25 +18,36 @@ using osu.Framework;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows; using osu.Desktop.Windows;
using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Desktop namespace osu.Desktop
{ {
internal class OsuGameDesktop : OsuGame internal class OsuGameDesktop : OsuGame
{ {
public OsuGameDesktop(string[] args = null) private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
public OsuGameDesktop(string[]? args = null)
: base(args) : base(args)
{ {
} }
public override StableStorage GetStorageForStableInstall() public override StableStorage? GetStorageForStableInstall()
{ {
try try
{ {
if (Host is DesktopGameHost desktopHost) if (Host is DesktopGameHost desktopHost)
{ {
string stablePath = getStableInstallPath(); string? stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath)) if (!string.IsNullOrEmpty(stablePath))
return new StableStorage(stablePath, desktopHost); return new StableStorage(stablePath, desktopHost);
} }
@ -51,11 +60,11 @@ namespace osu.Desktop
return null; return null;
} }
private string getStableInstallPath() private string? getStableInstallPath()
{ {
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg")); static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
string stableInstallPath; string? stableInstallPath;
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
@ -83,15 +92,15 @@ namespace osu.Desktop
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private string getStableInstallPathFromRegistry() private string? getStableInstallPathFromRegistry()
{ {
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
} }
protected override UpdateManager CreateUpdateManager() protected override UpdateManager CreateUpdateManager()
{ {
string packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
if (!string.IsNullOrEmpty(packageManaged)) if (!string.IsNullOrEmpty(packageManaged))
return new NoActionUpdateManager(); return new NoActionUpdateManager();
@ -118,6 +127,8 @@ namespace osu.Desktop
LoadComponentAsync(new GameplayWinKeyBlocker(), Add); LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add); LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
} }
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
@ -134,8 +145,29 @@ namespace osu.Desktop
desktopWindow.DragDrop += f => fileDrop(new[] { f }); desktopWindow.DragDrop += f => fileDrop(new[] { f });
} }
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case ITabletHandler th:
return new TabletSettings(th);
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler th:
return new InputSection.HandlerSection(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private readonly List<string> importableFiles = new List<string>(); private readonly List<string> importableFiles = new List<string>();
private ScheduledDelegate importSchedule; private ScheduledDelegate? importSchedule;
private void fileDrop(string[] filePaths) private void fileDrop(string[] filePaths)
{ {
@ -168,5 +200,11 @@ namespace osu.Desktop
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose();
}
} }
} }

View File

@ -11,8 +11,10 @@ using osu.Framework;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using SDL2;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -28,7 +30,27 @@ namespace osu.Desktop
{ {
// run Squirrel first, as the app may exit after these run // run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
// disabling it ourselves.
// We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!",
"This version of osu! requires at least Windows 8.1 to run.\n"
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
return;
}
setupSquirrel(); setupSquirrel();
}
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory; string cwd = Environment.CurrentDirectory;
@ -65,19 +87,8 @@ namespace osu.Desktop
{ {
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args if (trySendIPCMessage(host, cwd, args))
{
var importer = new ArchiveImportIPCChannel(host);
foreach (string file in args)
{
Console.WriteLine(@"Importing {0}", file);
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
throw new TimeoutException(@"IPC took too long to send");
}
return; return;
}
// we want to allow multiple instances to be started when in debug. // we want to allow multiple instances to be started when in debug.
if (!DebugUtils.IsDebugBuild) if (!DebugUtils.IsDebugBuild)
@ -108,21 +119,49 @@ namespace osu.Desktop
} }
} }
private static bool trySendIPCMessage(IIpcHost host, string cwd, string[] args)
{
if (args.Length == 1 && args[0].StartsWith(OsuGameBase.OSU_PROTOCOL, StringComparison.Ordinal))
{
var osuSchemeLinkHandler = new OsuSchemeLinkIPCChannel(host);
if (!osuSchemeLinkHandler.HandleLinkAsync(args[0]).Wait(3000))
throw new IPCTimeoutException(osuSchemeLinkHandler.GetType());
return true;
}
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
{
var importer = new ArchiveImportIPCChannel(host);
foreach (string file in args)
{
Console.WriteLine(@"Importing {0}", file);
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
throw new IPCTimeoutException(importer.GetType());
}
return true;
}
return false;
}
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private static void setupSquirrel() private static void setupSquirrel()
{ {
SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) => SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
{ {
tools.CreateShortcutForThisExe(); tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
}, onAppUpdate: (version, tools) => }, onAppUpdate: (_, tools) =>
{ {
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
}, onAppUninstall: (version, tools) => }, onAppUninstall: (_, tools) =>
{ {
tools.RemoveShortcutForThisExe(); tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry(); tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) => }, onEveryRun: (_, _, _) =>
{ {
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly. // causes the right-click context menu to function incorrectly.

View File

@ -154,7 +154,7 @@ namespace osu.Desktop.Updater
Activated = () => Activated = () =>
{ {
updateManager.PrepareUpdateAsync() updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit())); .ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit()));
return true; return true;
}; };
} }

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.40" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO; using System.IO;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;

View File

@ -0,0 +1,166 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Benchmarks
{
public class BenchmarkHitObject : BenchmarkTest
{
[Params(1, 100, 1000)]
public int Count { get; set; }
[Params(false, true)]
public bool WithBindableAccess { get; set; }
[Benchmark]
public HitCircle[] OsuCircle()
{
var circles = new HitCircle[Count];
for (int i = 0; i < Count; i++)
{
circles[i] = new HitCircle();
if (WithBindableAccess)
{
_ = circles[i].PositionBindable;
_ = circles[i].ScaleBindable;
_ = circles[i].ComboIndexBindable;
_ = circles[i].ComboOffsetBindable;
_ = circles[i].StackHeightBindable;
_ = circles[i].LastInComboBindable;
_ = circles[i].ComboIndexWithOffsetsBindable;
_ = circles[i].IndexInCurrentComboBindable;
_ = circles[i].SamplesBindable;
_ = circles[i].StartTimeBindable;
}
else
{
_ = circles[i].Position;
_ = circles[i].Scale;
_ = circles[i].ComboIndex;
_ = circles[i].ComboOffset;
_ = circles[i].StackHeight;
_ = circles[i].LastInCombo;
_ = circles[i].ComboIndexWithOffsets;
_ = circles[i].IndexInCurrentCombo;
_ = circles[i].Samples;
_ = circles[i].StartTime;
_ = circles[i].Position;
_ = circles[i].Scale;
_ = circles[i].ComboIndex;
_ = circles[i].ComboOffset;
_ = circles[i].StackHeight;
_ = circles[i].LastInCombo;
_ = circles[i].ComboIndexWithOffsets;
_ = circles[i].IndexInCurrentCombo;
_ = circles[i].Samples;
_ = circles[i].StartTime;
}
}
return circles;
}
[Benchmark]
public Hit[] TaikoHit()
{
var hits = new Hit[Count];
for (int i = 0; i < Count; i++)
{
hits[i] = new Hit();
if (WithBindableAccess)
{
_ = hits[i].TypeBindable;
_ = hits[i].IsStrongBindable;
_ = hits[i].SamplesBindable;
_ = hits[i].StartTimeBindable;
}
else
{
_ = hits[i].Type;
_ = hits[i].IsStrong;
_ = hits[i].Samples;
_ = hits[i].StartTime;
}
}
return hits;
}
[Benchmark]
public Fruit[] CatchFruit()
{
var fruit = new Fruit[Count];
for (int i = 0; i < Count; i++)
{
fruit[i] = new Fruit();
if (WithBindableAccess)
{
_ = fruit[i].OriginalXBindable;
_ = fruit[i].XOffsetBindable;
_ = fruit[i].ScaleBindable;
_ = fruit[i].ComboIndexBindable;
_ = fruit[i].HyperDashBindable;
_ = fruit[i].LastInComboBindable;
_ = fruit[i].ComboIndexWithOffsetsBindable;
_ = fruit[i].IndexInCurrentComboBindable;
_ = fruit[i].IndexInBeatmapBindable;
_ = fruit[i].SamplesBindable;
_ = fruit[i].StartTimeBindable;
}
else
{
_ = fruit[i].OriginalX;
_ = fruit[i].XOffset;
_ = fruit[i].Scale;
_ = fruit[i].ComboIndex;
_ = fruit[i].HyperDash;
_ = fruit[i].LastInCombo;
_ = fruit[i].ComboIndexWithOffsets;
_ = fruit[i].IndexInCurrentCombo;
_ = fruit[i].IndexInBeatmap;
_ = fruit[i].Samples;
_ = fruit[i].StartTime;
}
}
return fruit;
}
[Benchmark]
public Note[] ManiaNote()
{
var notes = new Note[Count];
for (int i = 0; i < Count; i++)
{
notes[i] = new Note();
if (WithBindableAccess)
{
_ = notes[i].ColumnBindable;
_ = notes[i].SamplesBindable;
_ = notes[i].StartTimeBindable;
}
else
{
_ = notes[i].Column;
_ = notes[i].Samples;
_ = notes[i].StartTime;
}
}
return notes;
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -11,7 +9,7 @@ namespace osu.Game.Benchmarks
{ {
public class BenchmarkMod : BenchmarkTest public class BenchmarkMod : BenchmarkTest
{ {
private OsuModDoubleTime mod; private OsuModDoubleTime mod = null!;
[Params(1, 10, 100)] [Params(1, 10, 100)]
public int Times { get; set; } public int Times { get; set; }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
@ -17,9 +15,9 @@ namespace osu.Game.Benchmarks
{ {
public class BenchmarkRealmReads : BenchmarkTest public class BenchmarkRealmReads : BenchmarkTest
{ {
private TemporaryNativeStorage storage; private TemporaryNativeStorage storage = null!;
private RealmAccess realm; private RealmAccess realm = null!;
private UpdateThread updateThread; private UpdateThread updateThread = null!;
[Params(1, 100, 1000)] [Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; } public int ReadsPerFetch { get; set; }
@ -31,7 +29,7 @@ namespace osu.Game.Benchmarks
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME); realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r => realm.Run(_ =>
{ {
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
}); });
@ -76,7 +74,7 @@ namespace osu.Game.Benchmarks
} }
}); });
done.Wait(); done.Wait(60000);
} }
[Benchmark] [Benchmark]
@ -115,7 +113,7 @@ namespace osu.Game.Benchmarks
} }
}); });
done.Wait(); done.Wait(60000);
} }
[Benchmark] [Benchmark]
@ -135,9 +133,9 @@ namespace osu.Game.Benchmarks
[GlobalCleanup] [GlobalCleanup]
public void Cleanup() public void Cleanup()
{ {
realm?.Dispose(); realm.Dispose();
storage?.Dispose(); storage.Dispose();
updateThread?.Exit(); updateThread.Exit();
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines; using BenchmarkDotNet.Engines;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -13,9 +11,9 @@ namespace osu.Game.Benchmarks
{ {
public class BenchmarkRuleset : BenchmarkTest public class BenchmarkRuleset : BenchmarkTest
{ {
private OsuRuleset ruleset; private OsuRuleset ruleset = null!;
private APIMod apiModDoubleTime; private APIMod apiModDoubleTime = null!;
private APIMod apiModDifficultyAdjust; private APIMod apiModDifficultyAdjust = null!;
public override void SetUp() public override void SetUp()
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;
using NUnit.Framework; using NUnit.Framework;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Configs; using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;

View File

@ -24,21 +24,24 @@ namespace osu.Game.Rulesets.Catch.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } }, new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
}; };
[TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(catch_mod_mapping))] [TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(catch_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new CatchRuleset(); protected override Ruleset CreateRuleset() => new CatchRuleset();

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new JuiceStreamPathVertex(20, -5) new JuiceStreamPathVertex(20, -5)
})); }));
removeCount = path.RemoveVertices((_, i) => true); removeCount = path.RemoveVertices((_, _) => true);
Assert.That(removeCount, Is.EqualTo(1)); Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[] Assert.That(path.Vertices, Is.EqualTo(new[]
{ {

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f)); AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit()); AddStep("exit player", () => Player.Exit());
CreateTest(null); CreateTest();
} }
AddAssert("legacy HUD combo counter hidden", () => AddAssert("legacy HUD combo counter hidden", () =>

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
hyperDashCount = 0; hyperDashCount = 0;
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => Player.DrawableRuleset.FrameStableComponents.OnUpdate += _ =>
{ {
var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault(); var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME; protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -1,10 +1,9 @@
// 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 disable
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
@ -31,9 +30,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
StarRating = values[ATTRIB_ID_AIM]; StarRating = values[ATTRIB_ID_AIM];
ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE];

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth; private float halfCatcherWidth;
public override int Version => 20220701;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {

View File

@ -135,15 +135,15 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
{ {
case SelectTool _: case SelectTool:
if (EditorBeatmap.SelectedHitObjects.Count == 0) if (EditorBeatmap.SelectedHitObjects.Count == 0)
return null; return null;
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime); double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
return getLastSnappableHitObject(minTime); return getLastSnappableHitObject(minTime);
case FruitCompositionTool _: case FruitCompositionTool:
case JuiceStreamCompositionTool _: case JuiceStreamCompositionTool:
if (!CursorInPlacementArea) if (!CursorInPlacementArea)
return null; return null;

View File

@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Catch.Edit
case Droplet droplet: case Droplet droplet:
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX); return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
case JuiceStream _: case JuiceStream:
return GetPositionRange(hitObject.NestedHitObjects); return GetPositionRange(hitObject.NestedHitObjects);
case BananaShower _: case BananaShower:
// A banana shower occupies the whole screen width. // A banana shower occupies the whole screen width.
return new PositionRange(0, CatchPlayfield.WIDTH); return new PositionRange(0, CatchPlayfield.WIDTH);

View File

@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
switch (hitObject) switch (hitObject)
{ {
case BananaShower _: case BananaShower:
return false; return false;
case JuiceStream juiceStream: case JuiceStream juiceStream:

View File

@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDoubleTime : ModDoubleTime public class CatchModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModFlashlight : ModFlashlight<CatchHitObject> public class CatchModFlashlight : ModFlashlight<CatchHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat public override BindableFloat SizeMultiplier { get; } = new BindableFloat

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor) public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
{ {

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject> public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
{ {
public override string Description => @"Play with fading fruits."; public override string Description => @"Play with fading fruits.";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
private const double fade_out_offset_multiplier = 0.6; private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44; private const double fade_out_duration_multiplier = 0.44;

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModNightcore : ModNightcore<CatchHitObject> public class CatchModNightcore : ModNightcore<CatchHitObject>
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -19,7 +19,9 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
public readonly Bindable<float> OriginalXBindable = new Bindable<float>(); private HitObjectProperty<float> originalX;
public Bindable<float> OriginalXBindable => originalX.Bindable;
/// <summary> /// <summary>
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
@ -31,18 +33,20 @@ namespace osu.Game.Rulesets.Catch.Objects
[JsonIgnore] [JsonIgnore]
public float X public float X
{ {
set => OriginalXBindable.Value = value; set => originalX.Value = value;
} }
public readonly Bindable<float> XOffsetBindable = new Bindable<float>(); private HitObjectProperty<float> xOffset;
public Bindable<float> XOffsetBindable => xOffset.Bindable;
/// <summary> /// <summary>
/// A random offset applied to the horizontal position, set by the beatmap processing. /// A random offset applied to the horizontal position, set by the beatmap processing.
/// </summary> /// </summary>
public float XOffset public float XOffset
{ {
get => XOffsetBindable.Value; get => xOffset.Value;
set => XOffsetBindable.Value = value; set => xOffset.Value = value;
} }
/// <summary> /// <summary>
@ -54,8 +58,8 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </remarks> /// </remarks>
public float OriginalX public float OriginalX
{ {
get => OriginalXBindable.Value; get => originalX.Value;
set => OriginalXBindable.Value = value; set => originalX.Value = value;
} }
/// <summary> /// <summary>
@ -69,59 +73,71 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TimePreempt { get; set; } = 1000; public double TimePreempt { get; set; } = 1000;
public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>(); private HitObjectProperty<int> indexInBeatmap;
public Bindable<int> IndexInBeatmapBindable => indexInBeatmap.Bindable;
public int IndexInBeatmap public int IndexInBeatmap
{ {
get => IndexInBeatmapBindable.Value; get => indexInBeatmap.Value;
set => IndexInBeatmapBindable.Value = value; set => indexInBeatmap.Value = value;
} }
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; } public int ComboOffset { get; set; }
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> indexInCurrentCombo;
public Bindable<int> IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public int IndexInCurrentCombo public int IndexInCurrentCombo
{ {
get => IndexInCurrentComboBindable.Value; get => indexInCurrentCombo.Value;
set => IndexInCurrentComboBindable.Value = value; set => indexInCurrentCombo.Value = value;
} }
public Bindable<int> ComboIndexBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndex;
public Bindable<int> ComboIndexBindable => comboIndex.Bindable;
public int ComboIndex public int ComboIndex
{ {
get => ComboIndexBindable.Value; get => comboIndex.Value;
set => ComboIndexBindable.Value = value; set => comboIndex.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndexWithOffsets;
public Bindable<int> ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets public int ComboIndexWithOffsets
{ {
get => ComboIndexWithOffsetsBindable.Value; get => comboIndexWithOffsets.Value;
set => ComboIndexWithOffsetsBindable.Value = value; set => comboIndexWithOffsets.Value = value;
} }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); private HitObjectProperty<bool> lastInCombo;
public Bindable<bool> LastInComboBindable => lastInCombo.Bindable;
/// <summary> /// <summary>
/// The next fruit starts a new combo. Used for explodey. /// The next fruit starts a new combo. Used for explodey.
/// </summary> /// </summary>
public virtual bool LastInCombo public virtual bool LastInCombo
{ {
get => LastInComboBindable.Value; get => lastInCombo.Value;
set => LastInComboBindable.Value = value; set => lastInCombo.Value = value;
} }
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1); private HitObjectProperty<float> scale = new HitObjectProperty<float>(1);
public Bindable<float> ScaleBindable => scale.Bindable;
public float Scale public float Scale
{ {
get => ScaleBindable.Value; get => scale.Value;
set => ScaleBindable.Value = value; set => scale.Value = value;
} }
/// <summary> /// <summary>

View File

@ -5,6 +5,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public float DistanceToHyperDash { get; set; } public float DistanceToHyperDash { get; set; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>(); private HitObjectProperty<bool> hyperDash;
public Bindable<bool> HyperDashBindable => hyperDash.Bindable;
/// <summary> /// <summary>
/// Whether this fruit can initiate a hyperdash. /// Whether this fruit can initiate a hyperdash.
/// </summary> /// </summary>
public bool HyperDash => HyperDashBindable.Value; public bool HyperDash => hyperDash.Value;
private CatchHitObject hyperDashTarget; private CatchHitObject hyperDashTarget;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Utils; using osu.Framework.Utils;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Replays
{ {
} }
public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame lastFrame = null) public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame? lastFrame = null)
: base(time) : base(time)
{ {
Position = position ?? -1; Position = position ?? -1;
@ -40,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Replays
} }
} }
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{ {
Position = currentFrame.Position.X; Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1; Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;

View File

@ -389,13 +389,13 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
switch (source) switch (source)
{ {
case Fruit _: case Fruit:
return caughtFruitPool.Get(); return caughtFruitPool.Get();
case Banana _: case Banana:
return caughtBananaPool.Get(); return caughtBananaPool.Get();
case Droplet _: case Droplet:
return caughtDropletPool.Get(); return caughtDropletPool.Get();
default: default:

View File

@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
lastHyperDashState = Catcher.HyperDashing; lastHyperDashState = Catcher.HyperDashing;
} }
public void SetCatcherPosition(float X) public void SetCatcherPosition(float x)
{ {
float lastPosition = Catcher.X; float lastPosition = Catcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH); float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
Catcher.X = newPosition; Catcher.X = newPosition;

View File

@ -23,10 +23,8 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } }, new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } },
new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } }, new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } },
new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } }, new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } },
new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } }, new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } },
@ -34,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } }, new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } },
new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } }, new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } },
new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } }, new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } },
new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } },
new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } }, new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } },
new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } }, new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } },
new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } }, new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } },
@ -45,12 +42,18 @@ namespace osu.Game.Rulesets.Mania.Tests
}; };
[TestCaseSource(nameof(mania_mod_mapping))] [TestCaseSource(nameof(mania_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(mania_mod_mapping))] [TestCaseSource(nameof(mania_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
switch (original) switch (original)
{ {
case IHasDistance _: case IHasDistance:
{ {
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
conversion = generator; conversion = generator;

View File

@ -1,10 +1,9 @@
// 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 disable
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Mania.Difficulty namespace osu.Game.Rulesets.Mania.Difficulty
@ -20,12 +19,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("great_hit_window")] [JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; } public double GreatHitWindow { get; set; }
/// <summary>
/// The score multiplier applied via score-reducing mods.
/// </summary>
[JsonProperty("score_multiplier")]
public double ScoreMultiplier { get; set; }
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{ {
foreach (var v in base.ToDatabaseAttributes()) foreach (var v in base.ToDatabaseAttributes())
@ -34,17 +27,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
} }
} }
} }

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty; private readonly double originalOverallDifficulty;
public override int Version => 20220701;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
@ -53,7 +55,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// In osu-stable mania, rate-adjustment mods don't affect the hit window. // In osu-stable mania, rate-adjustment mods don't affect the hit window.
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate), GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject) MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
}; };
} }
@ -147,32 +148,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return value; return value;
} }
} }
private double getScoreMultiplier(Mod[] mods)
{
double scoreMultiplier = 1;
foreach (var m in mods)
{
switch (m)
{
case ManiaModNoFail _:
case ManiaModEasy _:
case ManiaModHalfTime _:
scoreMultiplier *= 0.5;
break;
}
}
var maniaBeatmap = (ManiaBeatmap)Beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0)
scoreMultiplier *= 0.9;
else if (diff < 0)
scoreMultiplier *= 0.9 + 0.04 * diff;
return scoreMultiplier;
}
} }
} }

View File

@ -14,19 +14,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
[JsonProperty("difficulty")] [JsonProperty("difficulty")]
public double Difficulty { get; set; } public double Difficulty { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("scaled_score")]
public double ScaledScore { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay() public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{ {
foreach (var attribute in base.GetAttributesForDisplay()) foreach (var attribute in base.GetAttributesForDisplay())
yield return attribute; yield return attribute;
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty); yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
} }
} }
} }

View File

@ -15,15 +15,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaPerformanceCalculator : PerformanceCalculator public class ManiaPerformanceCalculator : PerformanceCalculator
{ {
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
private int countPerfect; private int countPerfect;
private int countGreat; private int countGreat;
private int countGood; private int countGood;
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
private double scoreAccuracy;
public ManiaPerformanceCalculator() public ManiaPerformanceCalculator()
: base(new ManiaRuleset()) : base(new ManiaRuleset())
@ -34,82 +32,47 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
var maniaAttributes = (ManiaDifficultyAttributes)attributes; var maniaAttributes = (ManiaDifficultyAttributes)attributes;
scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect); countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good); countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
scoreAccuracy = customAccuracy;
if (maniaAttributes.ScoreMultiplier > 0)
{
// Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
}
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed. // The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8; double multiplier = 8.0;
if (score.Mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9; multiplier *= 0.75;
if (score.Mods.Any(m => m is ModEasy)) if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5; multiplier *= 0.5;
double difficultyValue = computeDifficultyValue(maniaAttributes); double difficultyValue = computeDifficultyValue(maniaAttributes);
double accValue = computeAccuracyValue(difficultyValue, maniaAttributes); double totalValue = difficultyValue * multiplier;
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
Math.Pow(accValue, 1.1), 1.0 / 1.1
) * multiplier;
return new ManiaPerformanceAttributes return new ManiaPerformanceAttributes
{ {
Difficulty = difficultyValue, Difficulty = difficultyValue,
Accuracy = accValue,
ScaledScore = scaledScore,
Total = totalValue Total = totalValue
}; };
} }
private double computeDifficultyValue(ManiaDifficultyAttributes attributes) private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
if (scaledScore <= 500000)
difficultyValue = 0;
else if (scaledScore <= 600000)
difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
else if (scaledScore <= 700000)
difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
else if (scaledScore <= 800000)
difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
else if (scaledScore <= 900000)
difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
else
difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
return difficultyValue; return difficultyValue;
} }
private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{
if (attributes.GreatHitWindow <= 0)
return 0;
// Lots of arbitrary values from testing.
// 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)
* difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
return accuracyValue;
}
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
/// <summary>
/// Accuracy used to weight judgements independently from the score's actual accuracy.
/// </summary>
private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;

View File

@ -146,56 +146,56 @@ namespace osu.Game.Rulesets.Mania
{ {
switch (mod) switch (mod)
{ {
case ManiaModKey1 _: case ManiaModKey1:
value |= LegacyMods.Key1; value |= LegacyMods.Key1;
break; break;
case ManiaModKey2 _: case ManiaModKey2:
value |= LegacyMods.Key2; value |= LegacyMods.Key2;
break; break;
case ManiaModKey3 _: case ManiaModKey3:
value |= LegacyMods.Key3; value |= LegacyMods.Key3;
break; break;
case ManiaModKey4 _: case ManiaModKey4:
value |= LegacyMods.Key4; value |= LegacyMods.Key4;
break; break;
case ManiaModKey5 _: case ManiaModKey5:
value |= LegacyMods.Key5; value |= LegacyMods.Key5;
break; break;
case ManiaModKey6 _: case ManiaModKey6:
value |= LegacyMods.Key6; value |= LegacyMods.Key6;
break; break;
case ManiaModKey7 _: case ManiaModKey7:
value |= LegacyMods.Key7; value |= LegacyMods.Key7;
break; break;
case ManiaModKey8 _: case ManiaModKey8:
value |= LegacyMods.Key8; value |= LegacyMods.Key8;
break; break;
case ManiaModKey9 _: case ManiaModKey9:
value |= LegacyMods.Key9; value |= LegacyMods.Key9;
break; break;
case ManiaModDualStages _: case ManiaModDualStages:
value |= LegacyMods.KeyCoop; value |= LegacyMods.KeyCoop;
break; break;
case ManiaModFadeIn _: case ManiaModFadeIn:
value |= LegacyMods.FadeIn; value |= LegacyMods.FadeIn;
value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that. value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that.
break; break;
case ManiaModMirror _: case ManiaModMirror:
value |= LegacyMods.Mirror; value |= LegacyMods.Mirror;
break; break;
case ManiaModRandom _: case ManiaModRandom:
value |= LegacyMods.Random; value |= LegacyMods.Random;
break; break;
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
public enum ManiaSkinComponents public enum ManiaSkinComponents

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
var rng = new Random((int)Seed.Value); var rng = new Random((int)Seed.Value);
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList(); var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(_ => rng.Next()).ToList();
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]); beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
} }

View File

@ -13,12 +13,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{ {
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{ {
public readonly Bindable<int> ColumnBindable = new Bindable<int>(); private HitObjectProperty<int> column;
public Bindable<int> ColumnBindable => column.Bindable;
public virtual int Column public virtual int Column
{ {
get => ColumnBindable.Value; get => column.Value;
set => ColumnBindable.Value = value; set => column.Value = value;
} }
protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); protected override HitWindows CreateHitWindows() => new ManiaHitWindows();

View File

@ -1,10 +1,9 @@
// 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 disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -57,11 +56,11 @@ namespace osu.Game.Rulesets.Mania.Replays
{ {
switch (point) switch (point)
{ {
case HitPoint _: case HitPoint:
actions.Add(columnActions[point.Column]); actions.Add(columnActions[point.Column]);
break; break;
case ReleasePoint _: case ReleasePoint:
actions.Remove(columnActions[point.Column]); actions.Remove(columnActions[point.Column]);
break; break;
} }
@ -85,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Replays
} }
} }
private double calculateReleaseTime(HitObject currentObject, HitObject nextObject) private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject)
{ {
double endTime = currentObject.GetEndTime(); double endTime = currentObject.GetEndTime();
@ -96,10 +95,10 @@ namespace osu.Game.Rulesets.Mania.Replays
bool canDelayKeyUpFully = nextObject == null || bool canDelayKeyUpFully = nextObject == null ||
nextObject.StartTime > endTime + RELEASE_DELAY; nextObject.StartTime > endTime + RELEASE_DELAY;
return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9); return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
} }
protected override HitObject GetNextObject(int currentIndex) protected override HitObject? GetNextObject(int currentIndex)
{ {
int desiredColumn = Beatmap.HitObjects[currentIndex].Column; int desiredColumn = Beatmap.HitObjects[currentIndex].Column;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions); Actions.AddRange(actions);
} }
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap; var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -10,7 +10,6 @@ using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -41,10 +40,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private bool editorComponentsReady => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true
&& editor?.ChildrenOfType<Playfield>().FirstOrDefault()?.IsLoaded == true;
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestVelocityChangeSavesCorrectly(bool adjustVelocity) public void TestVelocityChangeSavesCorrectly(bool adjustVelocity)
@ -52,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
double? velocity = null; double? velocity = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
@ -91,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("exit", () => InputManager.Key(Key.Escape)); AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editorComponentsReady); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
AddAssert("slider has correct velocity", () => slider.Velocity == velocity); AddAssert("slider has correct velocity", () => slider.Velocity == velocity);

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Tests
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase(); skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>())) skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName }); .Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRepel : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestRepel(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModRepel
{
RepulsionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -0,0 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModSingleTap : OsuModTestScene
{
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press different key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after a break.
/// </summary>
[Test]
public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2000),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(500, 100),
},
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
}
},
ReplayFrames = new List<ReplayFrame>
{
// first press to start singletap lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
// press different key after break but before hit object.
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press different key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
lastResult = null; lastResult = null;
spinner = nextSpinner; spinner = nextSpinner;
spinner.OnNewResult += (o, result) => lastResult = result; spinner.OnNewResult += (_, result) => lastResult = result;
} }
return lastResult?.Type == HitResult.Great; return lastResult?.Type == HitResult.Great;
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
return false; return false;
spinner = nextSpinner; spinner = nextSpinner;
spinner.OnNewResult += (o, result) => results.Add(result); spinner.OnNewResult += (_, result) => results.Add(result);
results.Clear(); results.Clear();
} }

View File

@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, 206, "diffcalc-test")] [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, 45, "zero-length-sliders")] [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, 206, "diffcalc-test")] [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, 45, "zero-length-sliders")] [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")] [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")] [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -25,24 +25,27 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } }, new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
}; };
[TestCaseSource(nameof(osu_mod_mapping))] [TestCaseSource(nameof(osu_mod_mapping))]
[TestCase(LegacyMods.Cinema, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(osu_mod_mapping))] [TestCaseSource(nameof(osu_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new OsuRuleset(); protected override Ruleset CreateRuleset() => new OsuRuleset();

View File

@ -14,7 +14,6 @@ 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.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -93,10 +92,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.White); AddAssert("ball is white", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red); AddAssert("ball is red", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.Red);
} }
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)

View File

@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class TestSceneSliderFollowCircleInput : RateAdjustedBeatmapTestScene
{
private List<JudgementResult>? judgementResults;
private ScoreAccessibleReplayPlayer? currentPlayer;
[Test]
public void TestMaximumDistanceTrackingWithoutMovement(
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
float circleSize,
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
double velocity)
{
const double time_slider_start = 1000;
float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2;
float followCircleRadius = circleRadius * 1.2f;
performTest(new Beatmap<OsuHitObject>
{
HitObjects =
{
new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(followCircleRadius, 0),
}, followCircleRadius),
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
CircleSize = circleSize,
SliderTickRate = 1
},
Ruleset = new OsuRuleset().RulesetInfo
},
}, new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(-circleRadius + 1, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
});
AddAssert("Tracking kept", assertMaxJudge);
}
private bool assertMaxJudge() => judgementResults?.Any() == true && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
private void performTest(Beatmap<OsuHitObject> beatmap, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults?.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer?.ScoreProcessor.HasCompleted.Value == true);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -24,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -66,18 +67,25 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSlider = null; drawableSlider = null;
}); });
[SetUpSteps] protected override bool HasCustomSteps => true;
public override void SetUpSteps()
{
}
[TestCase(0)] [TestCase(0)]
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
[FlakyTest]
/*
* Fail rate around 0.15%
*
* TearDown : System.TimeoutException : "wait for seek to finish" timed out
* --TearDown
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
*/
public void TestSnakingEnabled(int sliderIndex) public void TestSnakingEnabled(int sliderIndex)
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps(); CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex); retrieveSlider(sliderIndex);
@ -98,10 +106,20 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase(0)] [TestCase(0)]
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
[FlakyTest]
/*
* Fail rate around 0.15%
*
* TearDown : System.TimeoutException : "wait for seek to finish" timed out
* --TearDown
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
*/
public void TestSnakingDisabled(int sliderIndex) public void TestSnakingDisabled(int sliderIndex)
{ {
AddStep("have autoplay", () => autoplay = true); AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps(); CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
retrieveSlider(sliderIndex); retrieveSlider(sliderIndex);
@ -121,8 +139,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
setSnaking(true); setSnaking(true);
base.SetUpSteps(); CreateTest();
// repeat might have a chance to update its position depending on where in the frame its hit, // repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality // so some leniency is allowed here instead of checking strict equality
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
@ -133,15 +150,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("disable autoplay", () => autoplay = false); AddStep("disable autoplay", () => autoplay = false);
setSnaking(true); setSnaking(true);
base.SetUpSteps(); CreateTest();
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
} }
private void retrieveSlider(int index) private void retrieveSlider(int index)
{ {
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider); addSeekStep(() => slider.StartTime);
AddUntilStep("retrieve drawable slider", () => AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
} }
@ -161,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Tests
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd; private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First(); private Vector2 getSliderStart() => getSliderCurve().First();
@ -205,16 +221,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
} }
private void addSeekStep(Func<Slider> slider) private void addSeekStep(Func<double> getTime)
{ {
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime)); AddStep("seek to time", () => Player.GameplayClockContainer.Seek(getTime()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(getTime(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private void addSeekStep(Func<double> time)
{
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() }; protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -108,13 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
// Reward for % distance slowed down compared to previous, paying attention to not award overlap velocityChangeBonus = overlapVelocityBuff * distRatio;
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
// Choose the largest bonus, multiplied by ratio.
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
// Penalize for rhythm changes. // Penalize for rhythm changes.
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using System; using System;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -33,14 +32,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// derive strainTime for calculation // derive strainTime for calculation
var osuCurrObj = (OsuDifficultyHitObject)current; var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
var osuNextObj = (OsuDifficultyHitObject)current.Next(0);
double strainTime = osuCurrObj.StrainTime; double strainTime = osuCurrObj.StrainTime;
double greatWindowFull = greatWindow * 2; double greatWindowFull = greatWindow * 2;
double speedWindowRatio = strainTime / greatWindowFull; double doubletapness = 1;
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) // Nerf doubletappable doubles.
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime) if (osuNextObj != null)
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio); {
double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime);
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / greatWindowFull), 2);
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
}
// Cap deltatime to the OD 300 hitwindow. // Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
@ -55,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double travelDistance = osuPrevObj?.TravelDistance ?? 0; double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance); double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime;
} }
} }
} }

View File

@ -1,12 +1,11 @@
// 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 disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -26,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("speed_difficulty")] [JsonProperty("speed_difficulty")]
public double SpeedDifficulty { get; set; } public double SpeedDifficulty { get; set; }
/// <summary>
/// The number of clickable objects weighted by difficulty.
/// Related to <see cref="SpeedDifficulty"/>
/// </summary>
[JsonProperty("speed_note_count")]
public double SpeedNoteCount { get; set; }
/// <summary> /// <summary>
/// The difficulty corresponding to the flashlight skill. /// The difficulty corresponding to the flashlight skill.
/// </summary> /// </summary>
@ -94,11 +100,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
base.FromDatabaseAttributes(values); base.FromDatabaseAttributes(values, onlineInfo);
AimDifficulty = values[ATTRIB_ID_AIM]; AimDifficulty = values[ATTRIB_ID_AIM];
SpeedDifficulty = values[ATTRIB_ID_SPEED]; SpeedDifficulty = values[ATTRIB_ID_SPEED];
@ -108,6 +115,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount;
SpinnerCount = onlineInfo.SpinnerCount;
} }
#region Newtonsoft.Json implicit ShouldSerialize() methods #region Newtonsoft.Json implicit ShouldSerialize() methods

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private const double difficulty_multiplier = 0.0675; private const double difficulty_multiplier = 0.0675;
private double hitWindowGreat; private double hitWindowGreat;
public override int Version => 20220701;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
{ {
@ -38,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier; double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
@ -75,6 +78,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Mods = mods, Mods = mods,
AimDifficulty = aimRating, AimDifficulty = aimRating,
SpeedDifficulty = speedRating, SpeedDifficulty = speedRating,
SpeedNoteCount = speedNotes,
FlashlightDifficulty = 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,

View File

@ -163,8 +163,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// Calculate accuracy assuming the worst case scenario
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled. /// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
/// </summary> /// </summary>
public class Flashlight : OsuStrainSkill public class Flashlight : StrainSkill
{ {
private readonly bool hasHiddenMod; private readonly bool hasHiddenMod;
@ -27,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double skillMultiplier => 0.05; private double skillMultiplier => 0.05;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
private double currentStrain; private double currentStrain;
@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain; return currentStrain;
} }
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
} }
} }

View File

@ -14,6 +14,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainSkill public abstract class OsuStrainSkill : StrainSkill
{ {
/// <summary>
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
/// May be overridden via <see cref="DifficultyMultiplier"/>.
/// </summary>
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill. /// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
@ -28,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations. /// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
/// </summary> /// </summary>
protected virtual double DifficultyMultiplier => 1.06; protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
protected OsuStrainSkill(Mod[] mods) protected OsuStrainSkill(Mod[] mods)
: base(mods) : base(mods)

View File

@ -7,6 +7,9 @@ using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
@ -25,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double DifficultyMultiplier => 1.04; protected override double DifficultyMultiplier => 1.04;
private readonly double greatWindow; private readonly double greatWindow;
private readonly List<double> objectStrains = new List<double>();
public Speed(Mod[] mods, double hitWindowGreat) public Speed(Mod[] mods, double hitWindowGreat)
: base(mods) : base(mods)
{ {
@ -37,12 +42,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueAt(DifficultyHitObject current) protected override double StrainValueAt(DifficultyHitObject current)
{ {
currentStrain *= strainDecay(current.DeltaTime); currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier; currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier;
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow); currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
return currentStrain * currentRhythm; double totalStrain = currentStrain * currentRhythm;
objectStrains.Add(totalStrain);
return totalStrain;
}
public double RelevantNoteCount()
{
if (objectStrains.Count == 0)
return 0;
double maxStrain = objectStrains.Max();
if (maxStrain == 0)
return 0;
return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
} }
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public class SliderPlacementBlueprint : PlacementBlueprint public class SliderPlacementBlueprint : PlacementBlueprint
{ {
public new Objects.Slider HitObject => (Objects.Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;
private SliderBodyPiece bodyPiece; private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece; private HitCirclePiece headCirclePiece;
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private IDistanceSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint() public SliderPlacementBlueprint()
: base(new Objects.Slider()) : base(new Slider())
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -82,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial: case SliderPlacementState.Initial:
BeginPlacement(); BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint; var nearestDifficultyPoint = editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)?
.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}); });
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid(); placementObject.ValueChanged += _ => updateDistanceSnapGrid();
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
{ {
case SelectTool _: case SelectTool:
if (!EditorBeatmap.SelectedHitObjects.Any()) if (!EditorBeatmap.SelectedHitObjects.Any())
return; return;

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
public override ModType Type => ModType.Conversion;
private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; }
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods = null!;
private IFrameStableClock gameplayClock = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
LastAcceptedAction = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (CheckValidNewAction(action))
{
LastAcceptedAction = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly InputBlockingMod mod;
public InputInterceptor(InputBlockingMod mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -1,119 +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 disable
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModAlternate : InputBlockingMod
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
private const double flash_duration = 1000; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
lastActionPressed = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableHitObject(DrawableHitObject drawable) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
drawable.ApplyCustomUpdateState += (drawableObject, state) => drawable.ApplyCustomUpdateState += (drawableObject, _) =>
{ {
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Adjust; public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds; private DrawableOsuBlinds blinds;

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap), typeof(OsuModRepel) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true); public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true); public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
@ -62,10 +59,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
switch (obj) switch (obj)
{ {
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head: case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value; head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break; break;

View File

@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDoubleTime : ModDoubleTime public class OsuModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120; private const double default_follow_delay = 120;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModHardRock : ModHardRock, IApplicableToHitObject public class OsuModHardRock : ModHardRock, IApplicableToHitObject
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool(); public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
public override string Description => @"Play with no approach circles and fading circles/sliders."; public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawableObject) switch (drawableObject)
{ {
case DrawableSliderTail _: case DrawableSliderTail:
using (drawableObject.BeginAbsoluteSequence(fadeStartTime)) using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
drawableObject.FadeOut(fadeDuration); drawableObject.FadeOut(fadeDuration);
@ -165,14 +165,14 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (hitObject) switch (hitObject)
{ {
case Slider _: case Slider:
return (fadeOutStartTime, longFadeDuration); return (fadeOutStartTime, longFadeDuration);
case SliderTick _: case SliderTick:
double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration); return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration);
case Spinner _: case Spinner:
return (fadeOutStartTime + longFadeDuration, fadeOutDuration); return (fadeOutStartTime + longFadeDuration, fadeOutDuration);
default: default:

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!"; public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock; private IFrameStableClock gameplayClock;

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNightcore : ModNightcore<OsuHitObject> public class OsuModNightcore : ModNightcore<OsuHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Mods
// apply grow effect // apply grow effect
switch (drawable) switch (drawable)
{ {
case DrawableSliderHead _: case DrawableSliderHead:
case DrawableSliderTail _: case DrawableSliderTail:
// special cases we should *not* be scaling. // special cases we should *not* be scaling.
break; break;
case DrawableSlider _: case DrawableSlider:
case DrawableHitCircle _: case DrawableHitCircle:
{ {
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);

View File

@ -3,11 +3,14 @@
#nullable disable #nullable disable
using System;
using System.Linq;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModPerfect : ModPerfect public class OsuModPerfect : ModPerfect
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
} }
} }

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