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

Merge remote-tracking branch 'origin/Liswiera-FL-changes' into Liswiera-FL-changes

This commit is contained in:
mk-56 2022-01-15 21:44:03 +01:00
commit ee4331dda4
653 changed files with 12915 additions and 5592 deletions

View File

@ -27,10 +27,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.725.0",
"version": "2021.1210.0",
"commands": [
"localisation"
]
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# Contributing Guidelines
Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
@ -32,7 +32,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* **Provide more information when asked to do so.**
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
* **When submitting a feature proposal, please describe it in the most understandable way you can.**
@ -54,7 +54,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
Here are some key things to note before jumping in:
@ -128,7 +128,7 @@ Here are some key things to note before jumping in:
* **Don't mistake criticism of code for criticism of your person.**
As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
* **Feel free to reach out for help.**

View File

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

View File

@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status
@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android;
using Android.App;
// used for AndroidBatteryInfo
[assembly: UsesPermission(Manifest.Permission.BatteryStats)]

View File

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

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// A difficulty calculation request from the legacy client.
/// </summary>
/// <remarks>
/// Synchronise any changes with osu!stable.
/// </remarks>
public class LegacyIpcDifficultyCalculationRequest
{
public string BeatmapFile { get; set; }
public int RulesetId { get; set; }
public int Mods { get; set; }
}
}

View File

@ -0,0 +1,16 @@
// 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.
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// A difficulty calculation response returned to the legacy client.
/// </summary>
/// <remarks>
/// Synchronise any changes with osu!stable.
/// </remarks>
public class LegacyIpcDifficultyCalculationResponse
{
public double StarRating { get; set; }
}
}

View File

@ -0,0 +1,53 @@
// 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.Platform;
using Newtonsoft.Json.Linq;
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// An <see cref="IpcMessage"/> that can be used to communicate to and from legacy clients.
/// <para>
/// In order to deserialise types at either end, types must be serialised as their <see cref="System.Type.AssemblyQualifiedName"/>,
/// however this cannot be done since osu!stable and osu!lazer live in two different assemblies.
/// <br />
/// To get around this, this class exists which serialises a payload (<see cref="LegacyIpcMessage.Data"/>) as an <see cref="System.Object"/> type,
/// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib).
/// The payload contains the data to be sent over the IPC channel.
/// <br />
/// At either end, Json.NET deserialises the payload into a <see cref="JObject"/> which is manually converted back into the expected <see cref="LegacyIpcMessage.Data"/> type,
/// which then further contains another <see cref="JObject"/> representing the data sent over the IPC channel whose type can likewise be lazily matched through
/// <see cref="LegacyIpcMessage.Data.MessageType"/>.
/// </para>
/// </summary>
/// <remarks>
/// Synchronise any changes with osu-stable.
/// </remarks>
public class LegacyIpcMessage : IpcMessage
{
public LegacyIpcMessage()
{
// Types/assemblies are not inter-compatible, so always serialise/deserialise into objects.
base.Type = typeof(object).FullName;
}
public new string Type => base.Type; // Hide setter.
public new object Value
{
get => base.Value;
set => base.Value = new Data
{
MessageType = value.GetType().Name,
MessageData = value
};
}
public class Data
{
public string MessageType { get; set; }
public object MessageData { get; set; }
}
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
#nullable enable
namespace osu.Desktop.LegacyIpc
{
/// <summary>
/// Provides IPC to legacy osu! clients.
/// </summary>
public class LegacyTcpIpcProvider : TcpIpcProvider
{
private static readonly Logger logger = Logger.GetLogger("legacy-ipc");
public LegacyTcpIpcProvider()
: base(45357)
{
MessageReceived += msg =>
{
try
{
logger.Add("Processing legacy IPC message...");
logger.Add($" {msg.Value}", LogLevel.Debug);
// See explanation in LegacyIpcMessage for why this is done this way.
var legacyData = ((JObject)msg.Value).ToObject<LegacyIpcMessage.Data>();
object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType);
return new LegacyIpcMessage
{
Value = onLegacyIpcMessageReceived(value)
};
}
catch (Exception ex)
{
logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex);
return null;
}
};
}
private object parseObject(JObject value, string type)
{
switch (type)
{
case nameof(LegacyIpcDifficultyCalculationRequest):
return value.ToObject<LegacyIpcDifficultyCalculationRequest>()
?? throw new InvalidOperationException($"Failed to parse request {value}");
case nameof(LegacyIpcDifficultyCalculationResponse):
return value.ToObject<LegacyIpcDifficultyCalculationResponse>()
?? throw new InvalidOperationException($"Failed to parse request {value}");
default:
throw new ArgumentException($"Unsupported object type {type}");
}
}
private object onLegacyIpcMessageReceived(object message)
{
switch (message)
{
case LegacyIpcDifficultyCalculationRequest req:
try
{
var ruleset = getLegacyRulesetFromID(req.RulesetId);
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
return new LegacyIpcDifficultyCalculationResponse
{
StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating
};
}
catch
{
return new LegacyIpcDifficultyCalculationResponse();
}
default:
throw new ArgumentException($"Unsupported message type {message}");
}
}
private static Ruleset getLegacyRulesetFromID(int rulesetId)
{
switch (rulesetId)
{
case 0:
return new OsuRuleset();
case 1:
return new TaikoRuleset();
case 2:
return new CatchRuleset();
case 3:
return new ManiaRuleset();
default:
throw new ArgumentException("Invalid ruleset id");
}
}
}
}

View File

@ -70,7 +70,9 @@ namespace osu.Desktop
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
}
catch { }
catch
{
}
}
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
@ -113,7 +115,7 @@ namespace osu.Desktop
base.LoadComplete();
if (!noVersionOverlay)
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
LoadComponentAsync(new DiscordRichPresence(), Add);

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
@ -18,8 +19,10 @@ namespace osu.Desktop
{
private const string base_game_name = @"osu";
private static LegacyTcpIpcProvider legacyIpc;
[STAThread]
public static int Main(string[] args)
public static void Main(string[] args)
{
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@ -69,14 +72,28 @@ namespace osu.Desktop
throw new TimeoutException(@"IPC took too long to send");
}
return 0;
return;
}
// we want to allow multiple instances to be started when in debug.
if (!DebugUtils.IsDebugBuild)
{
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
return 0;
return;
}
}
if (host.IsPrimaryInstance)
{
try
{
Logger.Log("Starting legacy IPC provider...");
legacyIpc = new LegacyTcpIpcProvider();
legacyIpc.Bind();
}
catch (Exception ex)
{
Logger.Error(ex, "Failed to start legacy IPC provider");
}
}
@ -84,8 +101,6 @@ namespace osu.Desktop
host.Run(new TournamentGame());
else
host.Run(new OsuGameDesktop(args));
return 0;
}
}

View File

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

View File

@ -14,6 +14,7 @@ namespace osu.Desktop.Windows
{
private Bindable<bool> disableWinKey;
private IBindable<bool> localUserPlaying;
private IBindable<bool> isActive;
[Resolved]
private GameHost host { get; set; }
@ -24,13 +25,16 @@ namespace osu.Desktop.Windows
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
isActive = host.IsActive.GetBoundCopy();
isActive.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
}
private void updateBlocking()
{
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

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

View File

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

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -14,7 +14,6 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
[Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@ -27,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
[TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
[TestCase("basic-hyperdash")]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
@ -70,6 +70,7 @@ namespace osu.Game.Rulesets.Catch.Tests
HitObject = hitObject;
startTime = 0;
position = 0;
hyperDash = false;
}
private double startTime;
@ -88,8 +89,17 @@ namespace osu.Game.Rulesets.Catch.Tests
set => position = value;
}
private bool hyperDash;
public bool HyperDash
{
get => (HitObject as PalpableCatchHitObject)?.HyperDash ?? hyperDash;
set => hyperDash = value;
}
public bool Equals(ConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(Position, other.Position, conversion_lenience);
&& Precision.AlmostEquals(Position, other.Position, conversion_lenience)
&& HyperDash == other.HyperDash;
}
}

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
public void TestJuicestream()
public void TestJuiceStream()
{
AddStep("hit juicestream", () => spawnJuiceStream(true));
AddUntilStep("wait for completion", () => playfieldIsEmpty);

View File

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

View File

@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
{
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo()));
var testSkinProvider = new SkinProvidingContainer(skin);
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceAttributes : PerformanceAttributes
{
}
}

View File

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

View File

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

View File

@ -0,0 +1,19 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 0,
"HyperDash": true
}]
},
{
"StartTime": 450,
"Objects": [{
"StartTime": 450,
"Position": 512,
"HyperDash": false
}]
}
]
}

View File

@ -0,0 +1,21 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 2
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:9.6
ApproachRate:9.6
SliderMultiplier:1.9
SliderTickRate:1
[TimingPoints]
2169,266.666666666667,4,2,1,70,1,0
[HitObjects]
0,192,369,1,0,0:0:0:0:
512,192,450,1,0,0:0:0:0:

View File

@ -3,147 +3,183 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 177
"Position": 177,
"HyperDash": false
},
{
"StartTime": 450,
"Position": 216.539276
"Position": 216.539276,
"HyperDash": false
},
{
"StartTime": 532,
"Position": 256.5667
"Position": 256.5667,
"HyperDash": false
},
{
"StartTime": 614,
"Position": 296.594116
"Position": 296.594116,
"HyperDash": false
},
{
"StartTime": 696,
"Position": 336.621521
"Position": 336.621521,
"HyperDash": false
},
{
"StartTime": 778,
"Position": 376.99762
"Position": 376.99762,
"HyperDash": false
},
{
"StartTime": 860,
"Position": 337.318878
"Position": 337.318878,
"HyperDash": false
},
{
"StartTime": 942,
"Position": 297.291443
"Position": 297.291443,
"HyperDash": false
},
{
"StartTime": 1024,
"Position": 257.264038
"Position": 257.264038,
"HyperDash": false
},
{
"StartTime": 1106,
"Position": 217.2366
"Position": 217.2366,
"HyperDash": false
},
{
"StartTime": 1188,
"Position": 177
"Position": 177,
"HyperDash": false
},
{
"StartTime": 1270,
"Position": 216.818192
"Position": 216.818192,
"HyperDash": false
},
{
"StartTime": 1352,
"Position": 256.8456
"Position": 256.8456,
"HyperDash": false
},
{
"StartTime": 1434,
"Position": 296.873047
"Position": 296.873047,
"HyperDash": false
},
{
"StartTime": 1516,
"Position": 336.900452
"Position": 336.900452,
"HyperDash": false
},
{
"StartTime": 1598,
"Position": 376.99762
"Position": 376.99762,
"HyperDash": false
},
{
"StartTime": 1680,
"Position": 337.039948
"Position": 337.039948,
"HyperDash": false
},
{
"StartTime": 1762,
"Position": 297.0125
"Position": 297.0125,
"HyperDash": false
},
{
"StartTime": 1844,
"Position": 256.9851
"Position": 256.9851,
"HyperDash": false
},
{
"StartTime": 1926,
"Position": 216.957672
"Position": 216.957672,
"HyperDash": false
},
{
"StartTime": 2008,
"Position": 177
"Position": 177,
"HyperDash": false
},
{
"StartTime": 2090,
"Position": 217.097137
"Position": 217.097137,
"HyperDash": false
},
{
"StartTime": 2172,
"Position": 257.124573
"Position": 257.124573,
"HyperDash": false
},
{
"StartTime": 2254,
"Position": 297.152
"Position": 297.152,
"HyperDash": false
},
{
"StartTime": 2336,
"Position": 337.179443
"Position": 337.179443,
"HyperDash": false
},
{
"StartTime": 2418,
"Position": 376.99762
"Position": 376.99762,
"HyperDash": false
},
{
"StartTime": 2500,
"Position": 336.760956
"Position": 336.760956,
"HyperDash": false
},
{
"StartTime": 2582,
"Position": 296.733643
"Position": 296.733643,
"HyperDash": false
},
{
"StartTime": 2664,
"Position": 256.7062
"Position": 256.7062,
"HyperDash": false
},
{
"StartTime": 2746,
"Position": 216.678772
"Position": 216.678772,
"HyperDash": false
},
{
"StartTime": 2828,
"Position": 177
"Position": 177,
"HyperDash": false
},
{
"StartTime": 2909,
"Position": 216.887909
"Position": 216.887909,
"HyperDash": false
},
{
"StartTime": 2991,
"Position": 256.915344
"Position": 256.915344,
"HyperDash": false
},
{
"StartTime": 3073,
"Position": 296.942749
"Position": 296.942749,
"HyperDash": false
},
{
"StartTime": 3155,
"Position": 336.970184
"Position": 336.970184,
"HyperDash": false
},
{
"StartTime": 3237,
"Position": 376.99762
"Position": 376.99762,
"HyperDash": false
}
]
}]

View File

@ -3,71 +3,88 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 65
"Position": 65,
"HyperDash": false
},
{
"StartTime": 450,
"Position": 482
"Position": 482,
"HyperDash": false
},
{
"StartTime": 532,
"Position": 164
"Position": 164,
"HyperDash": false
},
{
"StartTime": 614,
"Position": 315
"Position": 315,
"HyperDash": false
},
{
"StartTime": 696,
"Position": 145
"Position": 145,
"HyperDash": false
},
{
"StartTime": 778,
"Position": 159
"Position": 159,
"HyperDash": false
},
{
"StartTime": 860,
"Position": 310
"Position": 310,
"HyperDash": false
},
{
"StartTime": 942,
"Position": 441
"Position": 441,
"HyperDash": false
},
{
"StartTime": 1024,
"Position": 428
"Position": 428,
"HyperDash": false
},
{
"StartTime": 1106,
"Position": 243
"Position": 243,
"HyperDash": false
},
{
"StartTime": 1188,
"Position": 422
"Position": 422,
"HyperDash": false
},
{
"StartTime": 1270,
"Position": 481
"Position": 481,
"HyperDash": false
},
{
"StartTime": 1352,
"Position": 104
"Position": 104,
"HyperDash": false
},
{
"StartTime": 1434,
"Position": 473
"Position": 473,
"HyperDash": false
},
{
"StartTime": 1516,
"Position": 135
"Position": 135,
"HyperDash": false
},
{
"StartTime": 1598,
"Position": 360
"Position": 360,
"HyperDash": false
},
{
"StartTime": 1680,
"Position": 123
"Position": 123,
"HyperDash": false
}
]
}]

View File

@ -3,231 +3,264 @@
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 258
"Position": 258,
"HyperDash": false
}]
},
{
"StartTime": 450,
"Objects": [{
"StartTime": 450,
"Position": 254
"Position": 254,
"HyperDash": false
}]
},
{
"StartTime": 532,
"Objects": [{
"StartTime": 532,
"Position": 241
"Position": 241,
"HyperDash": false
}]
},
{
"StartTime": 614,
"Objects": [{
"StartTime": 614,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 696,
"Objects": [{
"StartTime": 696,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 778,
"Objects": [{
"StartTime": 778,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 860,
"Objects": [{
"StartTime": 860,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 942,
"Objects": [{
"StartTime": 942,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 1024,
"Objects": [{
"StartTime": 1024,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 1106,
"Objects": [{
"StartTime": 1106,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 1188,
"Objects": [{
"StartTime": 1188,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 1270,
"Objects": [{
"StartTime": 1270,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 1352,
"Objects": [{
"StartTime": 1352,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 1434,
"Objects": [{
"StartTime": 1434,
"Position": 258
"Position": 258,
"HyperDash": false
}]
},
{
"StartTime": 1516,
"Objects": [{
"StartTime": 1516,
"Position": 253
"Position": 253,
"HyperDash": false
}]
},
{
"StartTime": 1598,
"Objects": [{
"StartTime": 1598,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 1680,
"Objects": [{
"StartTime": 1680,
"Position": 260
"Position": 260,
"HyperDash": false
}]
},
{
"StartTime": 1762,
"Objects": [{
"StartTime": 1762,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 1844,
"Objects": [{
"StartTime": 1844,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 1926,
"Objects": [{
"StartTime": 1926,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 2008,
"Objects": [{
"StartTime": 2008,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 2090,
"Objects": [{
"StartTime": 2090,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 2172,
"Objects": [{
"StartTime": 2172,
"Position": 243
"Position": 243,
"HyperDash": false
}]
},
{
"StartTime": 2254,
"Objects": [{
"StartTime": 2254,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 2336,
"Objects": [{
"StartTime": 2336,
"Position": 278
"Position": 278,
"HyperDash": false
}]
},
{
"StartTime": 2418,
"Objects": [{
"StartTime": 2418,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 2500,
"Objects": [{
"StartTime": 2500,
"Position": 258
"Position": 258,
"HyperDash": false
}]
},
{
"StartTime": 2582,
"Objects": [{
"StartTime": 2582,
"Position": 256
"Position": 256,
"HyperDash": false
}]
},
{
"StartTime": 2664,
"Objects": [{
"StartTime": 2664,
"Position": 242
"Position": 242,
"HyperDash": false
}]
},
{
"StartTime": 2746,
"Objects": [{
"StartTime": 2746,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 2828,
"Objects": [{
"StartTime": 2828,
"Position": 238
"Position": 238,
"HyperDash": false
}]
},
{
"StartTime": 2909,
"Objects": [{
"StartTime": 2909,
"Position": 271
"Position": 271,
"HyperDash": false
}]
},
{
"StartTime": 2991,
"Objects": [{
"StartTime": 2991,
"Position": 254
"Position": 254,
"HyperDash": false
}]
}
]

View File

@ -3,14 +3,16 @@
"StartTime": 3368,
"Objects": [{
"StartTime": 3368,
"Position": 374
"Position": 374,
"HyperDash": false
}]
},
{
"StartTime": 3501,
"Objects": [{
"StartTime": 3501,
"Position": 446
"Position": 446,
"HyperDash": false
}]
}
]

View File

@ -1 +1,71 @@
{"Mappings":[{"StartTime":19184.0,"Objects":[{"StartTime":19184.0,"Position":320.0},{"StartTime":19263.0,"Position":311.730255},{"StartTime":19343.0,"Position":324.6205},{"StartTime":19423.0,"Position":343.0907},{"StartTime":19503.0,"Position":372.2917},{"StartTime":19582.0,"Position":385.194733},{"StartTime":19662.0,"Position":379.0426},{"StartTime":19742.0,"Position":385.1066},{"StartTime":19822.0,"Position":391.624664},{"StartTime":19919.0,"Position":386.27832},{"StartTime":20016.0,"Position":380.117035},{"StartTime":20113.0,"Position":381.664154},{"StartTime":20247.0,"Position":370.872864}]}]}
{
"Mappings": [{
"StartTime": 19184,
"Objects": [{
"StartTime": 19184,
"Position": 320,
"HyperDash": false
},
{
"StartTime": 19263,
"Position": 311.730255,
"HyperDash": false
},
{
"StartTime": 19343,
"Position": 324.6205,
"HyperDash": false
},
{
"StartTime": 19423,
"Position": 343.0907,
"HyperDash": false
},
{
"StartTime": 19503,
"Position": 372.2917,
"HyperDash": false
},
{
"StartTime": 19582,
"Position": 385.194733,
"HyperDash": false
},
{
"StartTime": 19662,
"Position": 379.0426,
"HyperDash": false
},
{
"StartTime": 19742,
"Position": 385.1066,
"HyperDash": false
},
{
"StartTime": 19822,
"Position": 391.624664,
"HyperDash": false
},
{
"StartTime": 19919,
"Position": 386.27832,
"HyperDash": false
},
{
"StartTime": 20016,
"Position": 380.117035,
"HyperDash": false
},
{
"StartTime": 20113,
"Position": 381.664154,
"HyperDash": false
},
{
"StartTime": 20247,
"Position": 370.872864,
"HyperDash": false
}
]
}]
}

View File

@ -3,18 +3,21 @@
"StartTime": 2589,
"Objects": [{
"StartTime": 2589,
"Position": 256
"Position": 256,
"HyperDash": false
}]
},
{
"StartTime": 2915,
"Objects": [{
"StartTime": 2915,
"Position": 65
"Position": 65,
"HyperDash": false
},
{
"StartTime": 2916,
"Position": 482
"Position": 482,
"HyperDash": false
}
]
},
@ -22,11 +25,13 @@
"StartTime": 3078,
"Objects": [{
"StartTime": 3078,
"Position": 164
"Position": 164,
"HyperDash": false
},
{
"StartTime": 3079,
"Position": 315
"Position": 315,
"HyperDash": false
}
]
},
@ -34,11 +39,13 @@
"StartTime": 3241,
"Objects": [{
"StartTime": 3241,
"Position": 145
"Position": 145,
"HyperDash": false
},
{
"StartTime": 3242,
"Position": 159
"Position": 159,
"HyperDash": false
}
]
},
@ -46,11 +53,13 @@
"StartTime": 3404,
"Objects": [{
"StartTime": 3404,
"Position": 310
"Position": 310,
"HyperDash": false
},
{
"StartTime": 3405,
"Position": 441
"Position": 441,
"HyperDash": false
}
]
},
@ -58,7 +67,8 @@
"StartTime": 5197,
"Objects": [{
"StartTime": 5197,
"Position": 256
"Position": 256,
"HyperDash": false
}]
}
]

View File

@ -3,71 +3,88 @@
"StartTime": 18500,
"Objects": [{
"StartTime": 18500,
"Position": 65
"Position": 65,
"HyperDash": false
},
{
"StartTime": 18559,
"Position": 482
"Position": 482,
"HyperDash": false
},
{
"StartTime": 18618,
"Position": 164
"Position": 164,
"HyperDash": false
},
{
"StartTime": 18678,
"Position": 315
"Position": 315,
"HyperDash": false
},
{
"StartTime": 18737,
"Position": 145
"Position": 145,
"HyperDash": false
},
{
"StartTime": 18796,
"Position": 159
"Position": 159,
"HyperDash": false
},
{
"StartTime": 18856,
"Position": 310
"Position": 310,
"HyperDash": false
},
{
"StartTime": 18915,
"Position": 441
"Position": 441,
"HyperDash": false
},
{
"StartTime": 18975,
"Position": 428
"Position": 428,
"HyperDash": false
},
{
"StartTime": 19034,
"Position": 243
"Position": 243,
"HyperDash": false
},
{
"StartTime": 19093,
"Position": 422
"Position": 422,
"HyperDash": false
},
{
"StartTime": 19153,
"Position": 481
"Position": 481,
"HyperDash": false
},
{
"StartTime": 19212,
"Position": 104
"Position": 104,
"HyperDash": false
},
{
"StartTime": 19271,
"Position": 473
"Position": 473,
"HyperDash": false
},
{
"StartTime": 19331,
"Position": 135
"Position": 135,
"HyperDash": false
},
{
"StartTime": 19390,
"Position": 360
"Position": 360,
"HyperDash": false
},
{
"StartTime": 19450,
"Position": 123
"Position": 123,
"HyperDash": false
}
]
}]

View File

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

View File

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

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

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

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit;
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
public void TestLegacySkin()
{
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceAttributes : PerformanceAttributes
{
[JsonProperty("difficulty")]
public double Difficulty { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("scaled_score")]
public double ScaledScore { get; set; }
}
}

View File

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

View File

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

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.UI;
@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> working { get; set; }
[Resolved]
private OsuColour colours { get; set; }

View File

@ -7,16 +7,12 @@ using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Mania.Edit
{
public class ManiaSelectionHandler : EditorSelectionHandler
{
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
private HitObjectComposer composer { get; set; }

View File

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

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaHealthProcessor : DrainingHealthProcessor
{
/// <inheritdoc/>
public ManiaHealthProcessor(double drainStartTime, double drainLenience = 0)
: base(drainStartTime, drainLenience)
{
}
protected override HitResult GetSimulatedHitResult(Judgement judgement)
{
// Users are not expected to attain perfect judgements for all notes due to the tighter hit window.
return judgement.MaxResult == HitResult.Perfect ? HitResult.Great : judgement.MaxResult;
}
}
}

View File

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

View File

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

View File

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

View File

@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI
public JudgementResult Result { get; private set; }
[Resolved]
private Column column { get; set; }
private SkinnableDrawable skinnableExplosion;
public PoolableHitExplosion()

View File

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

View File

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

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceAttributes : PerformanceAttributes
{
[JsonProperty("aim")]
public double Aim { get; set; }
[JsonProperty("speed")]
public double Speed { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("flashlight")]
public double Flashlight { get; set; }
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
public override PerformanceAttributes Calculate()
{
mods = Score.Mods;
accuracy = Score.Accuracy;
@ -45,11 +45,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut))
if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
@ -72,42 +71,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryRatings != null)
return new OsuPerformanceAttributes
{
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("Flashlight", flashlightValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
}
return totalValue;
Aim = aimValue,
Speed = speedValue,
Accuracy = accuracyValue,
Flashlight = flashlightValue,
EffectiveMissCount = effectiveMissCount,
Total = totalValue
};
}
private double computeAimValue()
{
double rawAim = Attributes.AimStrain;
double rawAim = Attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling.
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
aimValue *= getComboScalingFactor();
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
@ -136,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
aimValue *= accuracy;
// It is important to also consider accuracy difficulty when doing that.
// It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
@ -144,9 +136,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
@ -155,9 +146,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
speedValue *= getComboScalingFactor();
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
@ -227,14 +216,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!mods.Any(h => h is OsuModFlashlight))
return 0.0;
double rawFlashlight = Attributes.FlashlightRating;
double rawFlashlight = Attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
// Add an additional bonus for HDFL.
if (mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
@ -242,9 +230,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
flashlightValue *= getComboScalingFactor();
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -276,6 +262,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
}
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}

View File

@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
public class OsuDifficultyHitObject : DifficultyHitObject
{
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
private const float maximum_slider_radius = normalized_radius * 2.4f;
private const float assumed_slider_radius = normalized_radius * 1.8f;
private const float maximum_slider_radius = normalised_radius * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double JumpDistance { get; private set; }
public readonly double StrainTime;
/// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// <para>
/// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
/// </para>
/// </summary>
public double MovementDistance { get; private set; }
public double LazyJumpDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// Normalised shortest distance to consider for a jump between the previous <see cref="OsuDifficultyHitObject"/> and this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
/// <remarks>
/// This is bounded from above by <see cref="LazyJumpDistance"/>, and is smaller than the former if a more natural path is able to be taken through the previous <see cref="OsuDifficultyHitObject"/>.
/// </remarks>
/// <example>
/// Suppose a linear slider - circle pattern.
/// <br />
/// Following the slider lazily (see: <see cref="LazyJumpDistance"/>) will result in underestimating the true end position of the slider as being closer towards the start position.
/// As a result, <see cref="LazyJumpDistance"/> overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
/// such that the jump is felt as only starting from the slider's true end position.
/// <br />
/// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
/// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
/// </example>
public double MinimumJumpDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="MinimumJumpDistance"/>, with a minimum value of 25ms.
/// </summary>
public double MinimumJumpTime { get; private set; }
/// <summary>
/// Normalised distance between the start and end position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double TravelDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary>
public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances(double clockRate)
{
if (BaseObject is Slider currentSlider)
{
computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance;
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
}
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner)
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
float scalingFactor = normalised_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
}
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
MinimumJumpTime = StrainTime;
MinimumJumpDistance = LazyJumpDistance;
if (lastObject is Slider lastSlider)
{
computeSliderCursorPosition(lastSlider);
TravelDistance = lastSlider.LazyTravelDistance;
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
//
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
//
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
//
// <======o==> ← slider
// | ← most natural jump path
// o ← a follow-up hitcircle
//
// In this case the most natural jump path is approximated by LazyJumpDistance.
//
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
//
// <======o==>---o
// ↑
// most natural jump path
//
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
//
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
//
// Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
// For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
// such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
// In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
// Additional distance is removed based on position of jump relative to slider follow circle radius.
// JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible.
MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
else
{
MovementTime = StrainTime;
MovementDistance = JumpDistance;
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
if (lastLastObject != null && !(lastLastObject is Spinner))
@ -139,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{
@ -167,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalized_radius;
requiredMovement = normalised_radius;
}
if (currMovementLength > requiredMovement)

View File

@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
}
// As above, do the same for the previous hitobject.
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
}
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Math.Max(prevVelocity, currVelocity) != 0)
{
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime;
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
// Scale with ratio of difference compared to 0.5 * max dist.
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2);
* 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;
@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
}
if (osuCurrObj.TravelTime != 0)
if (osuLastObj.TravelTime != 0)
{
// Reward sliders based on velocity.
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
}
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
}
private double skillMultiplier => 0.15;
private double skillMultiplier => 0.07;
private double strainDecayBase => 0.15;
protected override double DecayWeight => 1.0;
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
@ -40,26 +40,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double result = 0.0;
OsuDifficultyHitObject lastObj = osuCurrent;
// This is iterating backwards in time from the current object.
for (int i = 0; i < Previous.Count; i++)
{
var osuPrevious = (OsuDifficultyHitObject)Previous[i];
var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);
var currentObj = (OsuDifficultyHitObject)Previous[i];
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
if (!(osuPrevious.BaseObject is Spinner))
if (!(currentObj.BaseObject is Spinner))
{
double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
cumulativeStrainTime += osuPrevious.StrainTime;
cumulativeStrainTime += lastObj.StrainTime;
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
if (i == 0)
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}
lastObj = currentObj;
}
return Math.Pow(smallDistNerf * result, 2.0);

View File

@ -55,73 +55,75 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
bool firstDeltaSwitch = false;
for (int i = Previous.Count - 2; i > 0; i--)
int rhythmStart = 0;
while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
rhythmStart++;
for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
if (currHistoricalDecay != 0)
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
{
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else
{
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
else
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
startRatio = effectiveRatio;
islandSize = 1;
}
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
@ -154,7 +156,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,10 @@
using System.Collections.Generic;
using System.ComponentModel;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@ -39,6 +41,19 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e);
}
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
{
if (!AllowUserCursorMovement)
{
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
// Primarily relied upon by the "autopilot" osu! mod.
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
}
return base.HandleMouseTouchStateChange(e);
}
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
public bool AllowUserPresses = true;

View File

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

View File

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

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