mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 17:30:00 +08:00
Compare commits
1015 Commits
@@ -11,6 +11,10 @@ body:
|
||||
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
|
||||
- And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
|
||||
|
||||
# ATTENTION LINUX USERS
|
||||
|
||||
If you are having an issue and it is hardware related, **please open a [q&a discussion](https://github.com/ppy/osu/discussions/categories/q-a)** instead of an issue. There's a high chance your issue is due to your system configuration, and not our software.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
@@ -38,7 +42,7 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
|
||||
description: The version you encountered this bug on. This is shown at the end of the settings overlay.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
@@ -46,22 +50,16 @@ body:
|
||||
value: |
|
||||
## Logs
|
||||
|
||||
Attaching log files is required for every reported bug. See instructions below on how to find them.
|
||||
|
||||
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
|
||||
Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them.
|
||||
|
||||
### Desktop platforms
|
||||
|
||||
If the game has not yet been closed since you found the bug:
|
||||
1. Head on to game settings and click on "Open osu! folder"
|
||||
2. Then open the `logs` folder located there
|
||||
1. Head on to game settings and click on "Export logs"
|
||||
2. Click the notification to locate the file
|
||||
3. Drag the generated `.zip` files into the github issue window
|
||||
|
||||
The default places to find the logs on desktop platforms are as follows:
|
||||
- `%AppData%/osu/logs` *on Windows*
|
||||
- `~/.local/share/osu/logs` *on Linux*
|
||||
- `~/Library/Application Support/osu/logs` *on macOS*
|
||||
|
||||
If you have selected a custom location for the game files, you can find the `logs` folder there.
|
||||

|
||||
|
||||
### Mobile platforms
|
||||
|
||||
@@ -69,10 +67,6 @@ body:
|
||||
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
|
||||
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
||||
|
||||
---
|
||||
|
||||
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
|
||||
{
|
||||
host.Run(new OsuTestBrowser());
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
|
||||
{
|
||||
host.Run(new OsuTestBrowser());
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
|
||||
{
|
||||
host.Run(new OsuTestBrowser());
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Pippidon.Tests
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu"))
|
||||
{
|
||||
host.Run(new OsuTestBrowser());
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1213.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.114.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
+6
-7
@@ -16,15 +16,14 @@
|
||||
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
|
||||
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
|
||||
"osu.Game\\osu.Game.csproj",
|
||||
|
||||
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
|
||||
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
||||
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
|
||||
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
|
||||
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj"
|
||||
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
|
||||
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,739 @@
|
||||
// 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 disable
|
||||
|
||||
#pragma warning disable IDE1006 // Naming rule violation
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal static class NVAPI
|
||||
{
|
||||
private const string osu_filename = "osu!.exe";
|
||||
|
||||
// This is a good reference:
|
||||
// https://github.com/errollw/Warp-and-Blend-Quadros/blob/master/WarpBlend-Quadros/UnwarpAll-Quadros/include/nvapi.h
|
||||
// Note our Stride == their VERSION (e.g. NVDRS_SETTING_VER)
|
||||
|
||||
public const int MAX_PHYSICAL_GPUS = 64;
|
||||
public const int UNICODE_STRING_MAX = 2048;
|
||||
|
||||
public const string APPLICATION_NAME = @"osu!";
|
||||
public const string PROFILE_NAME = @"osu!";
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus EnumPhysicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
|
||||
|
||||
public static readonly EnumPhysicalGPUsDelegate EnumPhysicalGPUs;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus EnumLogicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
|
||||
|
||||
public static readonly EnumLogicalGPUsDelegate EnumLogicalGPUs;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetSystemTypeDelegate(IntPtr gpuHandle, out NvSystemType systemType);
|
||||
|
||||
public static readonly GetSystemTypeDelegate GetSystemType;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetGPUTypeDelegate(IntPtr gpuHandle, out NvGpuType gpuType);
|
||||
|
||||
public static readonly GetGPUTypeDelegate GetGPUType;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus CreateSessionDelegate(out IntPtr sessionHandle);
|
||||
|
||||
public static CreateSessionDelegate CreateSession;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus LoadSettingsDelegate(IntPtr sessionHandle);
|
||||
|
||||
public static LoadSettingsDelegate LoadSettings;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus FindApplicationByNameDelegate(IntPtr sessionHandle, [MarshalAs(UnmanagedType.BStr)] string appName, out IntPtr profileHandle, ref NvApplication application);
|
||||
|
||||
public static FindApplicationByNameDelegate FindApplicationByName;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetCurrentGlobalProfileDelegate(IntPtr sessionHandle, out IntPtr profileHandle);
|
||||
|
||||
public static GetCurrentGlobalProfileDelegate GetCurrentGlobalProfile;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetProfileInfoDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvProfile profile);
|
||||
|
||||
public static GetProfileInfoDelegate GetProfileInfo;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, NvSettingID settingID, ref NvSetting setting);
|
||||
|
||||
public static GetSettingDelegate GetSetting;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus CreateProfileDelegate(IntPtr sessionHandle, ref NvProfile profile, out IntPtr profileHandle);
|
||||
|
||||
private static readonly CreateProfileDelegate CreateProfile;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus SetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvSetting setting);
|
||||
|
||||
private static readonly SetSettingDelegate SetSetting;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus EnumApplicationsDelegate(IntPtr sessionHandle, IntPtr profileHandle, uint startIndex, ref uint appCount, [In, Out, MarshalAs(UnmanagedType.LPArray)] NvApplication[] applications);
|
||||
|
||||
private static readonly EnumApplicationsDelegate EnumApplications;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus CreateApplicationDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvApplication application);
|
||||
|
||||
private static readonly CreateApplicationDelegate CreateApplication;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus SaveSettingsDelegate(IntPtr sessionHandle);
|
||||
|
||||
private static readonly SaveSettingsDelegate SaveSettings;
|
||||
|
||||
public static NvStatus Status { get; private set; } = NvStatus.OK;
|
||||
public static bool Available { get; private set; }
|
||||
|
||||
private static IntPtr sessionHandle;
|
||||
|
||||
public static bool IsUsingOptimusDedicatedGpu
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return false;
|
||||
|
||||
if (!IsLaptop)
|
||||
return false;
|
||||
|
||||
IntPtr profileHandle;
|
||||
if (!getProfile(out profileHandle, out _, out bool _))
|
||||
return false;
|
||||
|
||||
// Get the optimus setting
|
||||
NvSetting setting;
|
||||
if (!getSetting(NvSettingID.SHIM_RENDERING_MODE_ID, profileHandle, out setting))
|
||||
return false;
|
||||
|
||||
return (setting.U32CurrentValue & (uint)NvShimSetting.SHIM_RENDERING_MODE_ENABLE) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsLaptop
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return false;
|
||||
|
||||
// Make sure that this is a laptop.
|
||||
var gpus = new IntPtr[64];
|
||||
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < gpuCount; i++)
|
||||
{
|
||||
if (checkError(GetSystemType(gpus[i], out var type)))
|
||||
return false;
|
||||
|
||||
if (type == NvSystemType.LAPTOP)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static NvThreadControlSetting ThreadedOptimisations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
IntPtr profileHandle;
|
||||
if (!getProfile(out profileHandle, out _, out bool _))
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Get the threaded optimisations setting
|
||||
NvSetting setting;
|
||||
if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting))
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
return (NvThreadControlSetting)setting.U32CurrentValue;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!Available)
|
||||
return;
|
||||
|
||||
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
|
||||
|
||||
Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the profile contains the current application.
|
||||
/// </summary>
|
||||
/// <returns>If the profile contains the current application.</returns>
|
||||
private static bool containsApplication(IntPtr profileHandle, NvProfile profile, out NvApplication application)
|
||||
{
|
||||
application = new NvApplication
|
||||
{
|
||||
Version = NvApplication.Stride
|
||||
};
|
||||
|
||||
if (profile.NumOfApps == 0)
|
||||
return false;
|
||||
|
||||
NvApplication[] applications = new NvApplication[profile.NumOfApps];
|
||||
applications[0].Version = NvApplication.Stride;
|
||||
|
||||
uint numApps = profile.NumOfApps;
|
||||
|
||||
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
|
||||
return false;
|
||||
|
||||
for (uint i = 0; i < numApps; i++)
|
||||
{
|
||||
if (applications[i].AppName == osu_filename)
|
||||
{
|
||||
application = applications[i];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the profile of the current application.
|
||||
/// </summary>
|
||||
/// <param name="profileHandle">The profile handle.</param>
|
||||
/// <param name="application">The current application description.</param>
|
||||
/// <param name="isApplicationSpecific">If this profile is not a global (default) profile.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool getProfile(out IntPtr profileHandle, out NvApplication application, out bool isApplicationSpecific)
|
||||
{
|
||||
application = new NvApplication
|
||||
{
|
||||
Version = NvApplication.Stride
|
||||
};
|
||||
|
||||
isApplicationSpecific = true;
|
||||
|
||||
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
|
||||
{
|
||||
isApplicationSpecific = false;
|
||||
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile.
|
||||
/// </summary>
|
||||
/// <param name="profileHandle">The profile handle.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool createProfile(out IntPtr profileHandle)
|
||||
{
|
||||
NvProfile newProfile = new NvProfile
|
||||
{
|
||||
Version = NvProfile.Stride,
|
||||
IsPredefined = 0,
|
||||
ProfileName = PROFILE_NAME,
|
||||
GPUSupport = new uint[32]
|
||||
};
|
||||
|
||||
newProfile.GPUSupport[0] = 1;
|
||||
|
||||
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a setting from the profile.
|
||||
/// </summary>
|
||||
/// <param name="settingId">The setting to retrieve.</param>
|
||||
/// <param name="profileHandle">The profile handle to retrieve the setting from.</param>
|
||||
/// <param name="setting">The setting.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out NvSetting setting)
|
||||
{
|
||||
setting = new NvSetting
|
||||
{
|
||||
Version = NvSetting.Stride,
|
||||
SettingID = settingId
|
||||
};
|
||||
|
||||
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool setSetting(NvSettingID settingId, uint settingValue)
|
||||
{
|
||||
NvApplication application;
|
||||
IntPtr profileHandle;
|
||||
bool isApplicationSpecific;
|
||||
if (!getProfile(out profileHandle, out application, out isApplicationSpecific))
|
||||
return false;
|
||||
|
||||
if (!isApplicationSpecific)
|
||||
{
|
||||
// We don't want to interfere with the user's other settings, so let's create a separate config for osu!
|
||||
if (!createProfile(out profileHandle))
|
||||
return false;
|
||||
}
|
||||
|
||||
NvSetting newSetting = new NvSetting
|
||||
{
|
||||
Version = NvSetting.Stride,
|
||||
SettingID = settingId,
|
||||
U32CurrentValue = settingValue
|
||||
};
|
||||
|
||||
// Set the thread state
|
||||
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
|
||||
return false;
|
||||
|
||||
// Get the profile (needed to check app count)
|
||||
NvProfile profile = new NvProfile
|
||||
{
|
||||
Version = NvProfile.Stride
|
||||
};
|
||||
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
|
||||
return false;
|
||||
|
||||
if (!containsApplication(profileHandle, profile, out application))
|
||||
{
|
||||
// Need to add the current application to the profile
|
||||
application.IsPredefined = 0;
|
||||
|
||||
application.AppName = osu_filename;
|
||||
application.UserFriendlyName = APPLICATION_NAME;
|
||||
|
||||
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save!
|
||||
return !checkError(SaveSettings(sessionHandle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a session to access the driver configuration.
|
||||
/// </summary>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool createSession()
|
||||
{
|
||||
if (checkError(CreateSession(out sessionHandle)))
|
||||
return false;
|
||||
|
||||
// Load settings into session
|
||||
if (checkError(LoadSettings(sessionHandle)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool checkError(NvStatus status)
|
||||
{
|
||||
Status = status;
|
||||
return status != NvStatus.OK;
|
||||
}
|
||||
|
||||
static NVAPI()
|
||||
{
|
||||
// TODO: check whether gpu vendor contains NVIDIA before attempting load?
|
||||
|
||||
try
|
||||
{
|
||||
// Try to load NVAPI
|
||||
if ((IntPtr.Size == 4 && loadLibrary(@"nvapi.dll") == IntPtr.Zero)
|
||||
|| (IntPtr.Size == 8 && loadLibrary(@"nvapi64.dll") == IntPtr.Zero))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeDelegate initialize;
|
||||
getDelegate(0x0150E828, out initialize);
|
||||
|
||||
if (initialize?.Invoke() == NvStatus.OK)
|
||||
{
|
||||
// IDs can be found here: https://github.com/jNizM/AHK_NVIDIA_NvAPI/blob/master/info/NvAPI_IDs.txt
|
||||
|
||||
getDelegate(0xE5AC921F, out EnumPhysicalGPUs);
|
||||
getDelegate(0x48B3EA59, out EnumLogicalGPUs);
|
||||
getDelegate(0xBAAABFCC, out GetSystemType);
|
||||
getDelegate(0xC33BAEB1, out GetGPUType);
|
||||
getDelegate(0x0694D52E, out CreateSession);
|
||||
getDelegate(0x375DBD6B, out LoadSettings);
|
||||
getDelegate(0xEEE566B2, out FindApplicationByName);
|
||||
getDelegate(0x617BFF9F, out GetCurrentGlobalProfile);
|
||||
getDelegate(0x577DD202, out SetSetting);
|
||||
getDelegate(0x61CD6FD6, out GetProfileInfo);
|
||||
getDelegate(0x73BF8338, out GetSetting);
|
||||
getDelegate(0xCC176068, out CreateProfile);
|
||||
getDelegate(0x7FA2173A, out EnumApplications);
|
||||
getDelegate(0x4347A9DE, out CreateApplication);
|
||||
getDelegate(0xFCBC7E14, out SaveSettings);
|
||||
}
|
||||
|
||||
if (createSession())
|
||||
Available = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void getDelegate<T>(uint id, out T newDelegate) where T : class
|
||||
{
|
||||
IntPtr ptr = IntPtr.Size == 4 ? queryInterface32(id) : queryInterface64(id);
|
||||
newDelegate = ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
|
||||
private static extern IntPtr loadLibrary(string dllToLoad);
|
||||
|
||||
[DllImport(@"nvapi.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr queryInterface32(uint id);
|
||||
|
||||
[DllImport(@"nvapi64.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr queryInterface64(uint id);
|
||||
|
||||
private delegate NvStatus InitializeDelegate();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct NvSetting
|
||||
{
|
||||
public uint Version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string SettingName;
|
||||
|
||||
public NvSettingID SettingID;
|
||||
public uint SettingType;
|
||||
public uint SettingLocation;
|
||||
public uint IsCurrentPredefined;
|
||||
public uint IsPredefinedValid;
|
||||
|
||||
public uint U32PredefinedValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string StringPredefinedValue;
|
||||
|
||||
public uint U32CurrentValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string StringCurrentValue;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetting)) | (1 << 16);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
|
||||
internal struct NvProfile
|
||||
{
|
||||
public uint Version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string ProfileName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray)]
|
||||
public uint[] GPUSupport;
|
||||
|
||||
public uint IsPredefined;
|
||||
public uint NumOfApps;
|
||||
public uint NumOfSettings;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvProfile)) | (1 << 16);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
|
||||
internal struct NvApplication
|
||||
{
|
||||
public uint Version;
|
||||
public uint IsPredefined;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string AppName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string UserFriendlyName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string Launcher;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string FileInFolder;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
|
||||
}
|
||||
|
||||
internal enum NvStatus
|
||||
{
|
||||
OK = 0, // Success. Request is completed.
|
||||
ERROR = -1, // Generic error
|
||||
LIBRARY_NOT_FOUND = -2, // NVAPI support library cannot be loaded.
|
||||
NO_IMPLEMENTATION = -3, // not implemented in current driver installation
|
||||
API_NOT_INITIALIZED = -4, // Initialize has not been called (successfully)
|
||||
INVALID_ARGUMENT = -5, // The argument/parameter value is not valid or NULL.
|
||||
NVIDIA_DEVICE_NOT_FOUND = -6, // No NVIDIA display driver, or NVIDIA GPU driving a display, was found.
|
||||
END_ENUMERATION = -7, // No more items to enumerate
|
||||
INVALID_HANDLE = -8, // Invalid handle
|
||||
INCOMPATIBLE_STRUCT_VERSION = -9, // An argument's structure version is not supported
|
||||
HANDLE_INVALIDATED = -10, // The handle is no longer valid (likely due to GPU or display re-configuration)
|
||||
OPENGL_CONTEXT_NOT_CURRENT = -11, // No NVIDIA OpenGL context is current (but needs to be)
|
||||
INVALID_POINTER = -14, // An invalid pointer, usually NULL, was passed as a parameter
|
||||
NO_GL_EXPERT = -12, // OpenGL Expert is not supported by the current drivers
|
||||
INSTRUMENTATION_DISABLED = -13, // OpenGL Expert is supported, but driver instrumentation is currently disabled
|
||||
NO_GL_NSIGHT = -15, // OpenGL does not support Nsight
|
||||
|
||||
EXPECTED_LOGICAL_GPU_HANDLE = -100, // Expected a logical GPU handle for one or more parameters
|
||||
EXPECTED_PHYSICAL_GPU_HANDLE = -101, // Expected a physical GPU handle for one or more parameters
|
||||
EXPECTED_DISPLAY_HANDLE = -102, // Expected an NV display handle for one or more parameters
|
||||
INVALID_COMBINATION = -103, // The combination of parameters is not valid.
|
||||
NOT_SUPPORTED = -104, // Requested feature is not supported in the selected GPU
|
||||
PORTID_NOT_FOUND = -105, // No port ID was found for the I2C transaction
|
||||
EXPECTED_UNATTACHED_DISPLAY_HANDLE = -106, // Expected an unattached display handle as one of the input parameters.
|
||||
INVALID_PERF_LEVEL = -107, // Invalid perf level
|
||||
DEVICE_BUSY = -108, // Device is busy; request not fulfilled
|
||||
NV_PERSIST_FILE_NOT_FOUND = -109, // NV persist file is not found
|
||||
PERSIST_DATA_NOT_FOUND = -110, // NV persist data is not found
|
||||
EXPECTED_TV_DISPLAY = -111, // Expected a TV output display
|
||||
EXPECTED_TV_DISPLAY_ON_DCONNECTOR = -112, // Expected a TV output on the D Connector - HDTV_EIAJ4120.
|
||||
NO_ACTIVE_SLI_TOPOLOGY = -113, // SLI is not active on this device.
|
||||
SLI_RENDERING_MODE_NOTALLOWED = -114, // Setup of SLI rendering mode is not possible right now.
|
||||
EXPECTED_DIGITAL_FLAT_PANEL = -115, // Expected a digital flat panel.
|
||||
ARGUMENT_EXCEED_MAX_SIZE = -116, // Argument exceeds the expected size.
|
||||
DEVICE_SWITCHING_NOT_ALLOWED = -117, // Inhibit is ON due to one of the flags in NV_GPU_DISPLAY_CHANGE_INHIBIT or SLI active.
|
||||
TESTING_CLOCKS_NOT_SUPPORTED = -118, // Testing of clocks is not supported.
|
||||
UNKNOWN_UNDERSCAN_CONFIG = -119, // The specified underscan config is from an unknown source (e.g. INF)
|
||||
TIMEOUT_RECONFIGURING_GPU_TOPO = -120, // Timeout while reconfiguring GPUs
|
||||
DATA_NOT_FOUND = -121, // Requested data was not found
|
||||
EXPECTED_ANALOG_DISPLAY = -122, // Expected an analog display
|
||||
NO_VIDLINK = -123, // No SLI video bridge is present
|
||||
REQUIRES_REBOOT = -124, // NVAPI requires a reboot for the settings to take effect
|
||||
INVALID_HYBRID_MODE = -125, // The function is not supported with the current Hybrid mode.
|
||||
MIXED_TARGET_TYPES = -126, // The target types are not all the same
|
||||
SYSWOW64_NOT_SUPPORTED = -127, // The function is not supported from 32-bit on a 64-bit system.
|
||||
IMPLICIT_SET_GPU_TOPOLOGY_CHANGE_NOT_ALLOWED = -128, // There is no implicit GPU topology active. Use SetHybridMode to change topology.
|
||||
REQUEST_USER_TO_CLOSE_NON_MIGRATABLE_APPS = -129, // Prompt the user to close all non-migratable applications.
|
||||
OUT_OF_MEMORY = -130, // Could not allocate sufficient memory to complete the call.
|
||||
WAS_STILL_DRAWING = -131, // The previous operation that is transferring information to or from this surface is incomplete.
|
||||
FILE_NOT_FOUND = -132, // The file was not found.
|
||||
TOO_MANY_UNIQUE_STATE_OBJECTS = -133, // There are too many unique instances of a particular type of state object.
|
||||
INVALID_CALL = -134, // The method call is invalid. For example, a method's parameter may not be a valid pointer.
|
||||
D3D10_1_LIBRARY_NOT_FOUND = -135, // d3d10_1.dll cannot be loaded.
|
||||
FUNCTION_NOT_FOUND = -136, // Couldn't find the function in the loaded DLL.
|
||||
INVALID_USER_PRIVILEGE = -137, // Current User is not Admin.
|
||||
EXPECTED_NON_PRIMARY_DISPLAY_HANDLE = -138, // The handle corresponds to GDIPrimary.
|
||||
EXPECTED_COMPUTE_GPU_HANDLE = -139, // Setting Physx GPU requires that the GPU is compute-capable.
|
||||
STEREO_NOT_INITIALIZED = -140, // The Stereo part of NVAPI failed to initialize completely. Check if the stereo driver is installed.
|
||||
STEREO_REGISTRY_ACCESS_FAILED = -141, // Access to stereo-related registry keys or values has failed.
|
||||
STEREO_REGISTRY_PROFILE_TYPE_NOT_SUPPORTED = -142, // The given registry profile type is not supported.
|
||||
STEREO_REGISTRY_VALUE_NOT_SUPPORTED = -143, // The given registry value is not supported.
|
||||
STEREO_NOT_ENABLED = -144, // Stereo is not enabled and the function needed it to execute completely.
|
||||
STEREO_NOT_TURNED_ON = -145, // Stereo is not turned on and the function needed it to execute completely.
|
||||
STEREO_INVALID_DEVICE_INTERFACE = -146, // Invalid device interface.
|
||||
STEREO_PARAMETER_OUT_OF_RANGE = -147, // Separation percentage or JPEG image capture quality is out of [0-100] range.
|
||||
STEREO_FRUSTUM_ADJUST_MODE_NOT_SUPPORTED = -148, // The given frustum adjust mode is not supported.
|
||||
TOPO_NOT_POSSIBLE = -149, // The mosaic topology is not possible given the current state of the hardware.
|
||||
MODE_CHANGE_FAILED = -150, // An attempt to do a display resolution mode change has failed.
|
||||
D3D11_LIBRARY_NOT_FOUND = -151, // d3d11.dll/d3d11_beta.dll cannot be loaded.
|
||||
INVALID_ADDRESS = -152, // Address is outside of valid range.
|
||||
STRING_TOO_SMALL = -153, // The pre-allocated string is too small to hold the result.
|
||||
MATCHING_DEVICE_NOT_FOUND = -154, // The input does not match any of the available devices.
|
||||
DRIVER_RUNNING = -155, // Driver is running.
|
||||
DRIVER_NOTRUNNING = -156, // Driver is not running.
|
||||
ERROR_DRIVER_RELOAD_REQUIRED = -157, // A driver reload is required to apply these settings.
|
||||
SET_NOT_ALLOWED = -158, // Intended setting is not allowed.
|
||||
ADVANCED_DISPLAY_TOPOLOGY_REQUIRED = -159, // Information can't be returned due to "advanced display topology".
|
||||
SETTING_NOT_FOUND = -160, // Setting is not found.
|
||||
SETTING_SIZE_TOO_LARGE = -161, // Setting size is too large.
|
||||
TOO_MANY_SETTINGS_IN_PROFILE = -162, // There are too many settings for a profile.
|
||||
PROFILE_NOT_FOUND = -163, // Profile is not found.
|
||||
PROFILE_NAME_IN_USE = -164, // Profile name is duplicated.
|
||||
PROFILE_NAME_EMPTY = -165, // Profile name is empty.
|
||||
EXECUTABLE_NOT_FOUND = -166, // Application not found in the Profile.
|
||||
EXECUTABLE_ALREADY_IN_USE = -167, // Application already exists in the other profile.
|
||||
DATATYPE_MISMATCH = -168, // Data Type mismatch
|
||||
PROFILE_REMOVED = -169, // The profile passed as parameter has been removed and is no longer valid.
|
||||
UNREGISTERED_RESOURCE = -170, // An unregistered resource was passed as a parameter.
|
||||
ID_OUT_OF_RANGE = -171, // The DisplayId corresponds to a display which is not within the normal outputId range.
|
||||
DISPLAYCONFIG_VALIDATION_FAILED = -172, // Display topology is not valid so the driver cannot do a mode set on this configuration.
|
||||
DPMST_CHANGED = -173, // Display Port Multi-Stream topology has been changed.
|
||||
INSUFFICIENT_BUFFER = -174, // Input buffer is insufficient to hold the contents.
|
||||
ACCESS_DENIED = -175, // No access to the caller.
|
||||
MOSAIC_NOT_ACTIVE = -176, // The requested action cannot be performed without Mosaic being enabled.
|
||||
SHARE_RESOURCE_RELOCATED = -177, // The surface is relocated away from video memory.
|
||||
REQUEST_USER_TO_DISABLE_DWM = -178, // The user should disable DWM before calling NvAPI.
|
||||
D3D_DEVICE_LOST = -179, // D3D device status is D3DERR_DEVICELOST or D3DERR_DEVICENOTRESET - the user has to reset the device.
|
||||
INVALID_CONFIGURATION = -180, // The requested action cannot be performed in the current state.
|
||||
STEREO_HANDSHAKE_NOT_DONE = -181, // Call failed as stereo handshake not completed.
|
||||
EXECUTABLE_PATH_IS_AMBIGUOUS = -182, // The path provided was too short to determine the correct NVDRS_APPLICATION
|
||||
DEFAULT_STEREO_PROFILE_IS_NOT_DEFINED = -183, // Default stereo profile is not currently defined
|
||||
DEFAULT_STEREO_PROFILE_DOES_NOT_EXIST = -184, // Default stereo profile does not exist
|
||||
CLUSTER_ALREADY_EXISTS = -185, // A cluster is already defined with the given configuration.
|
||||
DPMST_DISPLAY_ID_EXPECTED = -186, // The input display id is not that of a multi stream enabled connector or a display device in a multi stream topology
|
||||
INVALID_DISPLAY_ID = -187, // The input display id is not valid or the monitor associated to it does not support the current operation
|
||||
STREAM_IS_OUT_OF_SYNC = -188, // While playing secure audio stream, stream goes out of sync
|
||||
INCOMPATIBLE_AUDIO_DRIVER = -189, // Older audio driver version than required
|
||||
VALUE_ALREADY_SET = -190, // Value already set, setting again not allowed.
|
||||
TIMEOUT = -191, // Requested operation timed out
|
||||
GPU_WORKSTATION_FEATURE_INCOMPLETE = -192, // The requested workstation feature set has incomplete driver internal allocation resources
|
||||
STEREO_INIT_ACTIVATION_NOT_DONE = -193, // Call failed because InitActivation was not called.
|
||||
SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
|
||||
SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled.
|
||||
INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer.
|
||||
ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value.
|
||||
ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed.
|
||||
FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date.
|
||||
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
|
||||
}
|
||||
|
||||
internal enum NvSystemType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
LAPTOP = 1,
|
||||
DESKTOP = 2
|
||||
}
|
||||
|
||||
internal enum NvGpuType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
IGPU = 1, // Integrated
|
||||
DGPU = 2, // Discrete
|
||||
}
|
||||
|
||||
internal enum NvSettingID : uint
|
||||
{
|
||||
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
|
||||
OGL_DEEP_COLOR_SCANOUT_ID = 0x2097C2F6,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_ID = 0x206A6582,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_FRACTIONAL_ID = 0x206C4581,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_SIGN_ID = 0x20655CFA,
|
||||
OGL_EVENT_LOG_SEVERITY_THRESHOLD_ID = 0x209DF23E,
|
||||
OGL_EXTENSION_STRING_VERSION_ID = 0x20FF7493,
|
||||
OGL_FORCE_BLIT_ID = 0x201F619F,
|
||||
OGL_FORCE_STEREO_ID = 0x204D9A0C,
|
||||
OGL_IMPLICIT_GPU_AFFINITY_ID = 0x20D0F3E6,
|
||||
OGL_MAX_FRAMES_ALLOWED_ID = 0x208E55E3,
|
||||
OGL_MULTIMON_ID = 0x200AEBFC,
|
||||
OGL_OVERLAY_PIXEL_TYPE_ID = 0x209AE66F,
|
||||
OGL_OVERLAY_SUPPORT_ID = 0x206C28C4,
|
||||
OGL_QUALITY_ENHANCEMENTS_ID = 0x20797D6C,
|
||||
OGL_SINGLE_BACKDEPTH_BUFFER_ID = 0x20A29055,
|
||||
OGL_THREAD_CONTROL_ID = 0x20C1221E,
|
||||
OGL_TRIPLE_BUFFER_ID = 0x20FDD1F9,
|
||||
OGL_VIDEO_EDITING_MODE_ID = 0x20EE02B4,
|
||||
AA_BEHAVIOR_FLAGS_ID = 0x10ECDB82,
|
||||
AA_MODE_ALPHATOCOVERAGE_ID = 0x10FC2D9C,
|
||||
AA_MODE_GAMMACORRECTION_ID = 0x107D639D,
|
||||
AA_MODE_METHOD_ID = 0x10D773D2,
|
||||
AA_MODE_REPLAY_ID = 0x10D48A85,
|
||||
AA_MODE_SELECTOR_ID = 0x107EFC5B,
|
||||
AA_MODE_SELECTOR_SLIAA_ID = 0x107AFC5B,
|
||||
ANISO_MODE_LEVEL_ID = 0x101E61A9,
|
||||
ANISO_MODE_SELECTOR_ID = 0x10D2BB16,
|
||||
APPLICATION_PROFILE_NOTIFICATION_TIMEOUT_ID = 0x104554B6,
|
||||
APPLICATION_STEAM_ID_ID = 0x107CDDBC,
|
||||
CPL_HIDDEN_PROFILE_ID = 0x106D5CFF,
|
||||
CUDA_EXCLUDED_GPUS_ID = 0x10354FF8,
|
||||
D3DOGL_GPU_MAX_POWER_ID = 0x10D1EF29,
|
||||
EXPORT_PERF_COUNTERS_ID = 0x108F0841,
|
||||
FXAA_ALLOW_ID = 0x1034CB89,
|
||||
FXAA_ENABLE_ID = 0x1074C972,
|
||||
FXAA_INDICATOR_ENABLE_ID = 0x1068FB9C,
|
||||
MCSFRSHOWSPLIT_ID = 0x10287051,
|
||||
OPTIMUS_MAXAA_ID = 0x10F9DC83,
|
||||
PHYSXINDICATOR_ID = 0x1094F16F,
|
||||
PREFERRED_PSTATE_ID = 0x1057EB71,
|
||||
PREVENT_UI_AF_OVERRIDE_ID = 0x103BCCB5,
|
||||
PS_FRAMERATE_LIMITER_ID = 0x10834FEE,
|
||||
PS_FRAMERATE_LIMITER_GPS_CTRL_ID = 0x10834F01,
|
||||
SHIM_MAXRES_ID = 0x10F9DC82,
|
||||
SHIM_MCCOMPAT_ID = 0x10F9DC80,
|
||||
SHIM_RENDERING_MODE_ID = 0x10F9DC81,
|
||||
SHIM_RENDERING_OPTIONS_ID = 0x10F9DC84,
|
||||
SLI_GPU_COUNT_ID = 0x1033DCD1,
|
||||
SLI_PREDEFINED_GPU_COUNT_ID = 0x1033DCD2,
|
||||
SLI_PREDEFINED_GPU_COUNT_DX10_ID = 0x1033DCD3,
|
||||
SLI_PREDEFINED_MODE_ID = 0x1033CEC1,
|
||||
SLI_PREDEFINED_MODE_DX10_ID = 0x1033CEC2,
|
||||
SLI_RENDERING_MODE_ID = 0x1033CED1,
|
||||
VRRFEATUREINDICATOR_ID = 0x1094F157,
|
||||
VRROVERLAYINDICATOR_ID = 0x1095F16F,
|
||||
VRRREQUESTSTATE_ID = 0x1094F1F7,
|
||||
VSYNCSMOOTHAFR_ID = 0x101AE763,
|
||||
VSYNCVRRCONTROL_ID = 0x10A879CE,
|
||||
VSYNC_BEHAVIOR_FLAGS_ID = 0x10FDEC23,
|
||||
WKS_API_STEREO_EYES_EXCHANGE_ID = 0x11AE435C,
|
||||
WKS_API_STEREO_MODE_ID = 0x11E91A61,
|
||||
WKS_MEMORY_ALLOCATION_POLICY_ID = 0x11112233,
|
||||
WKS_STEREO_DONGLE_SUPPORT_ID = 0x112493BD,
|
||||
WKS_STEREO_SUPPORT_ID = 0x11AA9E99,
|
||||
WKS_STEREO_SWAP_MODE_ID = 0x11333333,
|
||||
AO_MODE_ID = 0x00667329,
|
||||
AO_MODE_ACTIVE_ID = 0x00664339,
|
||||
AUTO_LODBIASADJUST_ID = 0x00638E8F,
|
||||
ICAFE_LOGO_CONFIG_ID = 0x00DB1337,
|
||||
LODBIASADJUST_ID = 0x00738E8F,
|
||||
PRERENDERLIMIT_ID = 0x007BA09E,
|
||||
PS_DYNAMIC_TILING_ID = 0x00E5C6C0,
|
||||
PS_SHADERDISKCACHE_ID = 0x00198FFF,
|
||||
PS_TEXFILTER_ANISO_OPTS2_ID = 0x00E73211,
|
||||
PS_TEXFILTER_BILINEAR_IN_ANISO_ID = 0x0084CD70,
|
||||
PS_TEXFILTER_DISABLE_TRILIN_SLOPE_ID = 0x002ECAF2,
|
||||
PS_TEXFILTER_NO_NEG_LODBIAS_ID = 0x0019BB68,
|
||||
QUALITY_ENHANCEMENTS_ID = 0x00CE2691,
|
||||
REFRESH_RATE_OVERRIDE_ID = 0x0064B541,
|
||||
SET_POWER_THROTTLE_FOR_PCIe_COMPLIANCE_ID = 0x00AE785C,
|
||||
SET_VAB_DATA_ID = 0x00AB8687,
|
||||
VSYNCMODE_ID = 0x00A879CF,
|
||||
VSYNCTEARCONTROL_ID = 0x005A375C,
|
||||
TOTAL_DWORD_SETTING_NUM = 80,
|
||||
TOTAL_WSTRING_SETTING_NUM = 4,
|
||||
TOTAL_SETTING_NUM = 84,
|
||||
INVALID_SETTING_ID = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
internal enum NvShimSetting : uint
|
||||
{
|
||||
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
|
||||
SHIM_RENDERING_MODE_ENABLE = 0x00000001,
|
||||
SHIM_RENDERING_MODE_USER_EDITABLE = 0x00000002,
|
||||
SHIM_RENDERING_MODE_MASK = 0x00000003,
|
||||
SHIM_RENDERING_MODE_VIDEO_MASK = 0x00000004,
|
||||
SHIM_RENDERING_MODE_VARYING_BIT = 0x00000008,
|
||||
SHIM_RENDERING_MODE_AUTO_SELECT = 0x00000010,
|
||||
SHIM_RENDERING_MODE_OVERRIDE_BIT = 0x80000000,
|
||||
SHIM_RENDERING_MODE_NUM_VALUES = 8,
|
||||
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
|
||||
}
|
||||
|
||||
internal enum NvThreadControlSetting : uint
|
||||
{
|
||||
OGL_THREAD_CONTROL_ENABLE = 0x00000001,
|
||||
OGL_THREAD_CONTROL_DISABLE = 0x00000002,
|
||||
OGL_THREAD_CONTROL_NUM_VALUES = 2,
|
||||
OGL_THREAD_CONTROL_DEFAULT = 0
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -30,7 +30,19 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// run Squirrel first, as the app may exit after these run
|
||||
/*
|
||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
||||
*
|
||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
||||
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
||||
* namely by checking loaded assemblies:
|
||||
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
||||
*
|
||||
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
||||
* the app will then do completely broken things like:
|
||||
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
||||
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
||||
*/
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
@@ -54,6 +66,11 @@ namespace osu.Desktop
|
||||
setupSquirrel();
|
||||
}
|
||||
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
// Lazer and stable share the same executable name.
|
||||
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
|
||||
@@ -85,7 +102,7 @@ namespace osu.Desktop
|
||||
}
|
||||
}
|
||||
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null }))
|
||||
{
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.10.2" />
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3644427", new[] { typeof(CatchModEasy), typeof(CatchModFlashlight) })]
|
||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -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.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchRateAdjustedDisplayDifficultyTest
|
||||
{
|
||||
private static IEnumerable<float> difficultyValuesToTest()
|
||||
{
|
||||
for (float i = 0; i <= 10; i += 0.5f)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateBelowOne()
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateAboveOne()
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneCatchModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneCatchModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,582 @@
|
||||
osu file format v9
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:7
|
||||
CircleSize:5
|
||||
OverallDifficulty:8
|
||||
ApproachRate:8
|
||||
SliderMultiplier:3.2
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
//Break Periods
|
||||
2,16325,17625
|
||||
2,32325,33875
|
||||
2,66325,67375
|
||||
2,120135,127375
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
//Background Colour Transformations
|
||||
3,100,163,162,255
|
||||
|
||||
[TimingPoints]
|
||||
125,500,4,1,0,50,1,0
|
||||
36125,-100,4,1,0,50,0,1
|
||||
66125,-100,4,1,0,50,0,0
|
||||
88125,-100,4,1,0,50,0,1
|
||||
120125,-100,4,1,0,50,0,0
|
||||
170125,-100,4,2,0,5,0,0
|
||||
170250,-100,4,1,0,50,0,0
|
||||
172125,-100,4,1,0,50,0,1
|
||||
200125,-100,4,1,0,50,0,0
|
||||
|
||||
[HitObjects]
|
||||
64,80,2375,5,0
|
||||
172,192,2625,1,2
|
||||
152,36,2875,1,0
|
||||
80,176,3125,1,2
|
||||
224,112,3375,1,0
|
||||
192,256,3625,1,8
|
||||
136,116,3875,1,0
|
||||
272,32,4125,2,2,B|376:0|408:56|412:125|320:144|304:176|328:216|368:272|496:208,1,400,6|0
|
||||
504,216,4875,2,2,B|376:232|288:280|248:384,1,320
|
||||
384,344,5625,1,8
|
||||
272,216,5875,1,0
|
||||
272,216,6000,1,0
|
||||
272,216,6125,1,4
|
||||
92,280,6375,5,0
|
||||
124,108,6625,1,8
|
||||
256,8,6875,1,0
|
||||
388,108,7125,1,2
|
||||
420,280,7375,1,8
|
||||
256,296,7625,1,8
|
||||
256,120,7875,1,0
|
||||
443,152,8125,2,2,B|397:202|305:219|256:192|203:163|114:181|68:231,1,400,2|0
|
||||
24,256,8875,2,2,B|112:227|141:134|122:36|37:1,1,320
|
||||
16,132,9625,1,8
|
||||
136,280,9875,1,0
|
||||
136,280,10000,1,0
|
||||
136,280,10125,1,4
|
||||
256,172,10375,5,0
|
||||
368,56,10625,1,8
|
||||
196,116,10875,1,0
|
||||
316,116,11125,1,2
|
||||
144,56,11375,1,0
|
||||
256,0,11625,1,8
|
||||
112,128,11875,1,0
|
||||
164,280,12125,6,0,B|256:316,1,80,4|2
|
||||
100,348,12500,2,0,B|8:312,1,80,0|2
|
||||
144,212,12875,2,0,B|52:176,1,80,0|2
|
||||
208,144,13250,2,0,B|300:180,1,80,0|2
|
||||
332,324,13625,1,8
|
||||
180,324,13875,1,0
|
||||
256,240,14125,5,4
|
||||
256,240,14250,1,2
|
||||
324,112,14500,1,0
|
||||
324,112,14625,1,2
|
||||
192,56,14875,1,4
|
||||
192,56,15000,1,2
|
||||
256,164,15250,1,0
|
||||
256,164,15375,1,2
|
||||
256,20,15625,1,8
|
||||
120,56,15875,1,0
|
||||
256,92,16125,1,6
|
||||
20,152,18375,5,0
|
||||
180,136,18625,1,8
|
||||
52,228,18875,1,0
|
||||
120,84,19125,1,2
|
||||
128,244,19375,1,0
|
||||
48,84,19625,1,8
|
||||
192,212,19875,1,0
|
||||
300,72,20125,2,4,B|396:36|444:84|396:144|352:184|372:224|416:260|532:224|528:164,1,320,4|0
|
||||
472,40,20875,2,2,B|376:72|304:164|272:260|280:320,1,320
|
||||
404,352,21625,1,8
|
||||
432,196,21875,1,0
|
||||
432,196,22000,1,0
|
||||
432,196,22125,1,4
|
||||
296,100,22375,5,0
|
||||
168,196,22625,2,0,B|32:296,1,160,8|0
|
||||
268,212,23125,2,0,B|168:76,1,160,2|8
|
||||
252,312,23625,2,0,B|388:212,1,160,8|0
|
||||
484,96,24125,2,2,B|412:0|320:36|288:120|240:136|200:132|156:116|132:96|80:44,1,400,2|0
|
||||
72,24,24875,2,2,B|158:66|148:177|67:253|-19:210,1,320
|
||||
56,108,25625,1,8
|
||||
176,200,25875,1,0
|
||||
176,200,26000,1,0
|
||||
176,200,26125,1,4
|
||||
316,92,26375,5,0
|
||||
464,164,26625,2,0,B|394:224|412:336,1,160,2|0
|
||||
232,316,27125,2,0,B|306:256|284:144,1,160,2|8
|
||||
136,88,27625,1,8
|
||||
60,224,27875,1,0
|
||||
212,132,28125,6,0,B|256:32,1,80,4|2
|
||||
340,228,28500,2,0,B|384:128,1,80,0|2
|
||||
256,284,28875,2,0,B|212:184,1,80,4|2
|
||||
128,380,29250,2,0,B|84:280,1,80,0|2
|
||||
238,383,29625,2,0,B|406:379,1,160,8|0
|
||||
512,267,30125,5,4
|
||||
512,267,30250,1,2
|
||||
416,152,30500,1,0
|
||||
416,152,30625,1,2
|
||||
300,264,30875,1,4
|
||||
300,264,31000,1,2
|
||||
236,100,31250,1,0
|
||||
236,100,31375,1,2
|
||||
152,256,31625,1,8
|
||||
300,160,31875,1,0
|
||||
256,332,32125,1,6
|
||||
52,52,34625,5,0
|
||||
152,164,34875,1,0
|
||||
256,56,35125,1,4
|
||||
256,56,35625,1,2
|
||||
256,56,36125,2,4,B|331:63|364:136|320:224,1,160,4|0
|
||||
320,312,36625,1,8
|
||||
204,228,36875,1,0
|
||||
104,328,37125,2,2,B|24:287|44:188,1,160
|
||||
92,60,37625,1,8
|
||||
212,148,37875,1,0
|
||||
268,104,38000,1,0
|
||||
324,60,38125,2,0,B|452:184,1,160,4|0
|
||||
504,300,38625,1,8
|
||||
364,340,38875,1,0
|
||||
232,280,39125,6,2,B|150:282|69:198|105:87|179:53,2,320,2|2|6
|
||||
280,148,40375,1,0
|
||||
400,228,40625,2,0,B|520:368,1,160,8|0
|
||||
480,192,41125,1,2
|
||||
324,220,41375,1,2
|
||||
168,256,41625,1,8
|
||||
72,148,41875,1,2
|
||||
48,84,42000,1,2
|
||||
96,36,42125,2,0,B|164:108|256:44,1,160,6|0
|
||||
400,72,42625,1,2
|
||||
440,236,42875,1,2
|
||||
464,300,43000,1,2
|
||||
416,348,43125,2,0,B|348:276|256:340,1,160,6|0
|
||||
112,312,43625,1,2
|
||||
140,188,43875,1,0
|
||||
52,64,44125,5,6
|
||||
208,48,44375,1,0
|
||||
344,132,44625,1,8
|
||||
448,256,44875,2,2,B|401:321|285:337|217:242|233:163,2,320,2|2|0
|
||||
326,211,46125,2,2,B|279:146|163:130|95:225|111:304,1,320,6|0
|
||||
230,287,46875,2,2,B|277:352|393:368|461:273|445:194,1,320,6|8
|
||||
376,80,47625,1,8
|
||||
376,80,48125,6,0,B|304:128|216:96,1,160,4|0
|
||||
84,56,48625,1,8
|
||||
152,200,48875,1,0
|
||||
44,320,49125,2,0,B|121:364|204:320,1,160,4|0
|
||||
336,240,49625,5,8
|
||||
256,148,49875,1,0
|
||||
176,240,50125,1,0
|
||||
340,144,50625,1,0
|
||||
420,236,50875,1,0
|
||||
500,144,51125,1,2
|
||||
172,144,51625,1,2
|
||||
92,236,51875,1,0
|
||||
12,144,52125,6,0,B|160:48,1,160,4|0
|
||||
304,76,52625,1,8
|
||||
256,228,52875,1,0
|
||||
216,112,53125,2,0,B|364:208,1,160,2|0
|
||||
508,180,53625,1,8
|
||||
460,28,53875,1,0
|
||||
344,96,54125,1,2
|
||||
228,8,54375,1,0
|
||||
153,116,54625,1,2
|
||||
72,220,54875,1,0
|
||||
180,295,55125,1,2
|
||||
284,376,55375,1,0
|
||||
359,268,55625,1,2
|
||||
440,164,55875,1,0
|
||||
352,160,56125,6,0,B|466:294,1,160,4|0
|
||||
312,228,56625,1,8
|
||||
200,300,56875,1,0
|
||||
160,160,57125,2,0,B|46:294,1,160,4|0
|
||||
200,228,57625,1,8
|
||||
312,300,57875,1,0
|
||||
444,208,58125,2,0,B|362:164|380:56,1,160,2|0
|
||||
344,12,58500,1,0
|
||||
272,4,58625,2,0,B|232:88|120:68,1,160,2|0
|
||||
68,176,59125,2,0,B|148:220|132:328,1,160,2|0
|
||||
168,372,59500,1,0
|
||||
240,380,59625,2,0,B|280:296|392:316,1,160,2|0
|
||||
456,176,60125,5,6
|
||||
328,80,60375,1,0
|
||||
216,196,60625,1,8
|
||||
72,136,60875,2,2,B|54:209|91:305|191:336|269:306,2,320,2|2|0
|
||||
200,224,62125,2,2,B|182:150|219:54|319:23|397:53,1,320,2|0
|
||||
480,179,62875,2,2,B|499:252|462:348|362:379|284:349,1,320,2|0
|
||||
136,296,63625,2,0,B|67:220|140:136,1,160,8|0
|
||||
256,56,64125,5,6
|
||||
284,212,64375,1,0
|
||||
440,180,64625,1,8
|
||||
420,24,64875,1,0
|
||||
300,132,65125,1,6
|
||||
272,288,65375,1,0
|
||||
116,256,65625,1,8
|
||||
136,100,65875,1,0
|
||||
256,8,66125,1,4
|
||||
256,56,68125,6,0,B|298:128|244:237|123:241|74:173,1,320
|
||||
132,80,68875,2,2,B|344:328,1,320
|
||||
456,224,69625,1,8
|
||||
340,116,69875,1,0
|
||||
340,116,70000,1,0
|
||||
340,116,70125,1,4
|
||||
228,4,70375,5,0
|
||||
256,160,70625,2,0,B|186:224|88:168,1,160,2|0
|
||||
148,332,71125,2,0,B|216:396|316:340,1,160,2|8
|
||||
424,248,71625,1,8
|
||||
336,112,71875,1,0
|
||||
336,112,72000,1,0
|
||||
336,112,72125,1,4
|
||||
228,208,72375,2,0,B|139:179|144:80,1,160,0|8
|
||||
268,56,72875,2,2,B|272:164|220:272|120:308|72:308,1,320
|
||||
24,192,73625,1,8
|
||||
92,64,73875,1,0
|
||||
92,64,74000,1,0
|
||||
92,64,74125,1,4
|
||||
224,140,74375,5,0
|
||||
340,224,74625,2,0,B|412:211|428:121|363:77,1,160,2|0
|
||||
268,192,75125,2,0,B|196:205|180:295|245:339,1,160,2|0
|
||||
268,192,75625,2,0,B|104:168,1,160,8|0
|
||||
24,52,76125,6,0,B|132:40,1,80
|
||||
176,32,76375,1,2
|
||||
348,60,76625,1,2
|
||||
248,164,76875,1,2
|
||||
264,20,77125,1,2
|
||||
324,140,77375,1,2
|
||||
180,116,77625,1,2
|
||||
240,240,77875,1,0
|
||||
256,92,78125,1,4
|
||||
100,124,78375,5,0
|
||||
8,256,78625,2,0,B|64:332|176:304,1,160,8|0
|
||||
304,260,79125,2,0,B|248:184|136:212,1,160,2|0
|
||||
304,260,79625,1,8
|
||||
460,284,79875,1,2
|
||||
420,128,80125,6,0,B|332:128,1,80,4|0
|
||||
256,124,80375,1,2
|
||||
344,260,80625,1,2
|
||||
168,260,80875,1,2
|
||||
384,192,81125,1,2
|
||||
256,260,81375,1,2
|
||||
168,124,81625,1,2
|
||||
344,124,81875,1,2
|
||||
128,192,82125,1,4
|
||||
48,192,82250,6,0,B|48:84|152:52,1,160,2|0
|
||||
204,44,82625,2,0,B|204:152|308:184,1,160,2|0
|
||||
352,160,83000,2,0,B|244:160|212:264,1,160,2|0
|
||||
192,316,83375,2,0,B|84:316|52:212,1,160,2|2
|
||||
32,88,83875,1,2
|
||||
172,8,84125,1,4
|
||||
256,192,84250,12,6,86125
|
||||
256,192,86250,12,4,87125
|
||||
256,100,88125,6,2,B|308:116|368:104|404:16,1,160,6|0
|
||||
256,100,88625,1,8
|
||||
136,180,88875,1,0
|
||||
8,96,89125,2,0,B|-28:168|16:232|68:256,1,160,2|0
|
||||
164,312,89625,1,8
|
||||
288,236,89875,1,2
|
||||
288,236,90000,1,2
|
||||
288,236,90125,2,2,B|452:164,1,160,6|0
|
||||
476,32,90625,1,8
|
||||
332,104,90875,1,0
|
||||
180,104,91125,5,6
|
||||
36,32,91375,1,8
|
||||
56,164,91625,1,8
|
||||
56,164,92125,2,0,B|260:208,1,160,6|0
|
||||
84,296,92625,1,8
|
||||
220,376,92875,1,0
|
||||
320,268,93125,2,0,B|524:224,1,160,6|0
|
||||
432,80,93625,1,8
|
||||
296,152,93875,1,2
|
||||
296,152,94000,1,2
|
||||
296,152,94125,2,2,B|232:164|176:132|164:52,1,160,6|0
|
||||
216,232,94625,2,2,B|280:220|336:252|348:332,1,160,2|0
|
||||
341,304,95000,1,0
|
||||
341,304,95125,2,0,B|369:84,1,160,2|0
|
||||
171,80,95625,2,0,B|143:300,1,160,2|0
|
||||
43,358,96125,5,6
|
||||
81,219,96375,1,0
|
||||
169,332,96625,1,8
|
||||
304,272,96875,2,2,B|388:252|426:161|418:63|344:19,2,320,2|2|0
|
||||
240,144,98125,2,2,B|219:244|50:229|65:60|168:58,1,320
|
||||
240,144,98875,2,2,B|260:43|429:58|414:227|311:229,1,320,2|0
|
||||
180,292,99625,2,0,B|80:304|36:208,1,160,2|0
|
||||
48,64,100125,6,0,B|224:112,1,160,4|0
|
||||
348,52,100625,2,0,B|524:4,1,160,2|0
|
||||
504,172,101125,2,0,B|328:124,1,160,2|0
|
||||
204,184,101625,2,0,B|28:232,1,160,2|0
|
||||
49,226,102000,1,0
|
||||
49,226,102125,1,2
|
||||
256,324,102625,5,8
|
||||
384,256,102875,1,0
|
||||
256,188,103125,1,6
|
||||
256,188,103625,1,2
|
||||
128,256,103875,1,0
|
||||
256,324,104125,6,0,B|324:252|432:316,1,160,6|0
|
||||
492,168,104625,1,8
|
||||
332,188,104875,1,0
|
||||
256,60,105125,2,0,B|188:132|80:68,1,160,6|0
|
||||
20,216,105625,1,8
|
||||
180,196,105875,1,0
|
||||
368,156,106125,2,0,B|418:184|462:234|408:296,1,160,2|0
|
||||
220,80,106625,2,0,B|248:30|298:-14|360:40,1,160,2|0
|
||||
144,228,107125,2,0,B|94:200|50:150|104:88,1,160,2|0
|
||||
292,304,107625,2,0,B|264:354|214:398|152:344,1,160,2|0
|
||||
44,216,108125,6,0,B|145:221|172:132,1,160,6|0
|
||||
304,224,108625,1,8
|
||||
408,104,108875,1,0
|
||||
468,216,109125,2,0,B|367:221|340:132,1,160,6|0
|
||||
208,224,109625,1,8
|
||||
104,104,109875,1,0
|
||||
256,56,110125,2,0,B|144:180,1,160,2|0
|
||||
256,328,110625,2,0,B|368:204,1,160,2|0
|
||||
208,244,111125,2,0,B|96:368,1,160,2|0
|
||||
304,140,111625,2,0,B|416:16,1,160,2|0
|
||||
252,20,112125,5,6
|
||||
112,60,112375,1,0
|
||||
72,200,112625,1,8
|
||||
158,316,112875,2,2,B|236:321|324:259|326:152|278:89,2,320,2|2|0
|
||||
176,168,114125,2,2,B|214:236|313:276|405:220|431:145,1,320,2|0
|
||||
328,64,114875,2,2,B|259:102|219:201|275:293|350:319,1,320,2|0
|
||||
488,340,115625,2,0,B|456:172,1,160,2|0
|
||||
416,72,116125,5,6
|
||||
288,140,116375,1,0
|
||||
164,68,116625,1,8
|
||||
36,136,116875,1,0
|
||||
104,264,117125,1,6
|
||||
232,332,117375,1,0
|
||||
356,260,117625,1,8
|
||||
484,328,117875,1,0
|
||||
356,384,118125,1,6
|
||||
256,12,128125,5,4
|
||||
256,12,128250,1,2
|
||||
336,128,128500,1,0
|
||||
336,128,128625,1,2
|
||||
400,0,128875,1,0
|
||||
400,0,129000,1,2
|
||||
492,112,129250,1,0
|
||||
492,112,129375,1,2
|
||||
440,248,129625,2,2,B|272:284,1,160
|
||||
256,108,130125,5,4
|
||||
256,108,130250,1,2
|
||||
176,224,130500,1,0
|
||||
176,224,130625,1,2
|
||||
112,96,130875,1,0
|
||||
112,96,131000,1,2
|
||||
20,208,131250,1,0
|
||||
20,208,131375,1,2
|
||||
72,344,131625,2,2,B|240:380,1,160
|
||||
408,376,132125,6,0,B|512:352|584:248|592:-32|416:-48|256:-80|96:-16|56:88|8:224|88:304|144:336|184:368|256:368|256:368|328:368|368:336|424:304|504:224|456:88|416:-16|256:-80|96:-48|-80:-32|-72:248|0:352|104:376,1,2240,6|0
|
||||
256,192,135875,5,2
|
||||
256,192,136000,1,0
|
||||
256,192,136125,1,4
|
||||
136,104,136375,1,0
|
||||
132,240,136625,1,8
|
||||
133,240,136750,1,0
|
||||
256,280,137000,1,0
|
||||
255,280,137125,1,8
|
||||
256,280,137250,1,0
|
||||
256,280,137375,1,0
|
||||
380,240,137625,1,8
|
||||
376,104,137875,1,0
|
||||
256,124,138125,5,4
|
||||
256,124,138375,1,0
|
||||
144,192,138625,1,8
|
||||
144,192,138750,1,0
|
||||
256,260,139000,1,0
|
||||
256,260,139125,1,8
|
||||
256,260,139250,1,0
|
||||
256,260,139375,1,0
|
||||
368,192,139625,1,8
|
||||
256,124,139875,1,0
|
||||
256,124,140000,1,0
|
||||
256,124,140125,2,2,B|188:112|212:76|188:36|256:20,1,160,6|2
|
||||
332,128,140625,5,8
|
||||
332,128,140750,1,0
|
||||
332,256,141000,1,0
|
||||
332,256,141125,1,8
|
||||
332,256,141250,1,0
|
||||
332,256,141375,1,0
|
||||
180,256,141625,1,8
|
||||
180,128,141875,1,0
|
||||
256,56,142125,5,4
|
||||
256,56,142375,1,0
|
||||
256,160,142625,1,8
|
||||
256,160,142750,1,0
|
||||
256,264,143000,1,0
|
||||
256,264,143125,1,8
|
||||
256,264,143250,1,0
|
||||
256,264,143375,1,0
|
||||
188,352,143625,1,8
|
||||
324,352,143875,1,0
|
||||
324,352,144000,1,0
|
||||
324,352,144125,2,0,B|492:352,1,160,6|2
|
||||
392,280,144625,5,8
|
||||
392,280,144750,1,0
|
||||
324,192,145000,1,0
|
||||
324,192,145125,1,8
|
||||
324,192,145250,1,0
|
||||
324,192,145375,1,0
|
||||
188,192,145625,1,8
|
||||
120,280,145875,1,0
|
||||
256,288,146125,5,4
|
||||
256,288,146375,1,0
|
||||
256,176,146625,1,8
|
||||
256,176,146750,1,0
|
||||
176,96,147000,1,0
|
||||
176,96,147125,1,8
|
||||
176,96,147250,1,0
|
||||
176,96,147375,1,0
|
||||
256,16,147625,1,8
|
||||
336,96,147875,1,0
|
||||
336,96,148000,1,0
|
||||
336,96,148125,2,6,B|400:156|388:224|364:248,1,160,6|2
|
||||
256,272,148625,5,8
|
||||
240,264,148750,1,0
|
||||
240,180,149000,1,0
|
||||
256,172,149125,1,8
|
||||
272,164,149250,1,0
|
||||
288,156,149375,1,0
|
||||
256,64,149625,1,8
|
||||
256,64,149875,1,0
|
||||
116,180,150125,5,0
|
||||
120,200,150250,1,0
|
||||
132,224,150375,1,0
|
||||
152,236,150500,1,0
|
||||
176,240,150625,1,8
|
||||
208,240,150750,1,0
|
||||
232,236,150875,1,0
|
||||
248,216,151000,1,0
|
||||
256,192,151125,1,8
|
||||
260,168,151250,1,0
|
||||
272,144,151375,1,8
|
||||
292,132,151500,1,0
|
||||
316,128,151625,1,8
|
||||
348,128,151750,1,8
|
||||
372,132,151875,1,8
|
||||
388,152,152000,1,0
|
||||
404,184,152125,6,0,B|436:250|377:334|292:300,1,160,6|0
|
||||
108,200,152625,2,0,B|76:134|135:50|220:84,1,160,6|0
|
||||
256,192,153125,2,0,B|256:100,1,80,2|0
|
||||
256,192,153375,2,0,B|256:368,2,160,2|8|0
|
||||
360,60,154125,5,0
|
||||
360,60,154250,1,0
|
||||
360,60,154375,1,2
|
||||
256,12,154625,1,0
|
||||
256,12,154750,1,0
|
||||
256,12,154875,1,2
|
||||
154,64,155125,1,0
|
||||
154,64,155250,1,2
|
||||
155,63,155375,2,0,B|87:119|115:191|179:211|227:179,2,160,0|8|0
|
||||
163,74,156000,5,0
|
||||
163,74,156125,1,0
|
||||
163,74,156250,2,2,B|174:151|299:265|445:180|473:106,1,400,2|0
|
||||
320,80,157125,2,2,B|224:88|184:188|224:288|320:295,1,320
|
||||
348,292,157750,1,0
|
||||
380,280,157875,1,0
|
||||
404,260,158000,1,0
|
||||
412,236,158125,1,0
|
||||
412,208,158250,1,0
|
||||
404,180,158375,1,0
|
||||
264,68,158625,2,0,B|184:104,2,80,2|0|2
|
||||
164,216,159125,2,0,B|244:180,2,80,2|0|2
|
||||
56,144,159625,5,8
|
||||
64,276,159875,1,8
|
||||
64,276,160000,1,8
|
||||
64,276,160125,2,0,B|24:352,2,80,2|0|0
|
||||
128,288,160500,2,0,B|136:188,2,80,2|0|0
|
||||
192,300,160875,2,0,B|200:400,2,80,2|0|0
|
||||
240,256,161250,2,0,B|304:176,2,80,2|0|0
|
||||
284,304,161625,2,0,B|356:380,2,80,2|0|0
|
||||
328,256,162000,6,0,B|456:236,2,80,0|2|0
|
||||
308,192,162375,2,0,B|180:172,2,80,0|2|0
|
||||
340,136,162750,2,0,B|468:116,2,80,0|2|0
|
||||
284,100,163125,2,0,B|264:-28,2,80,0|2|0
|
||||
224,128,163500,2,0,B|204:256,2,80,0|2|0
|
||||
180,76,163875,6,0,B|92:52,2,80,2|0|0
|
||||
144,132,164250,2,0,B|72:184,2,80,2|0|0
|
||||
168,196,164625,2,0,B|240:248,2,80,2|0|0
|
||||
136,256,165000,2,0,B|96:340,2,80,2|0|0
|
||||
188,296,165375,2,0,B|228:380,2,80,2|0|0
|
||||
236,252,165750,1,0
|
||||
236,252,165875,1,2
|
||||
364,276,166125,6,2,B|408:176|360:156|320:168|296:176|268:132|264:112|272:76|304:52|328:40,1,240,2|0
|
||||
264,24,166625,2,2,B|308:124|260:144|220:132|196:124|168:168|164:188|172:224|204:248|228:260,1,240,2|0
|
||||
192,280,167125,1,0
|
||||
320,376,167375,1,0
|
||||
192,376,167625,1,0
|
||||
256,328,167750,1,0
|
||||
320,280,167875,1,0
|
||||
256,124,168125,1,6
|
||||
256,192,168250,12,0,170125
|
||||
256,192,171125,12,6,172125
|
||||
48,56,172375,5,0
|
||||
20,184,172625,2,0,B|16:264|92:316|152:304,1,160,8|0
|
||||
240,300,173125,1,2
|
||||
200,176,173375,1,0
|
||||
324,220,173625,2,0,B|360:220|416:258|412:338,1,160,8|0
|
||||
412,334,174000,1,0
|
||||
412,334,174125,2,0,B|456:156,1,160,6|0
|
||||
398,35,174625,2,0,B|220:-8,1,160,2|0
|
||||
245,0,175000,1,0
|
||||
245,0,175125,2,0,B|201:178,1,160,6|0
|
||||
259,299,175625,2,0,B|437:342,1,160,2|0
|
||||
424,176,176125,5,6
|
||||
272,128,176375,1,0
|
||||
116,152,176625,1,8
|
||||
173,253,176875,2,2,B|257:233|295:142|287:44|213:0,2,320,2|2|0
|
||||
28,204,178125,2,2,B|356:316,1,320
|
||||
172,360,178875,2,2,B|500:248,1,320,2|0
|
||||
384,148,179625,2,0,B|292:168|224:96|232:44,1,160,2|0
|
||||
244,93,180000,1,0
|
||||
244,93,180125,6,0,B|64:120,1,160,6|0
|
||||
100,268,180625,2,0,B|256:296,1,160,8|0
|
||||
257,296,181000,1,0
|
||||
256,296,181125,2,0,B|413:267,1,160,6|0
|
||||
426,116,181625,2,0,B|267:93,1,160,8|2
|
||||
267,93,182000,5,2
|
||||
267,93,182125,2,2,B|180:112|168:212,1,160,2|0
|
||||
140,380,182625,2,0,B|227:361|239:261,1,160,8|0
|
||||
62,169,183125,2,2,B|80:256|180:268,1,160,2|0
|
||||
348,296,183625,2,0,B|329:208|229:196,1,160,8|0
|
||||
64,172,184125,1,6
|
||||
256,192,184250,12,2,185625
|
||||
48,188,186125,6,2,B|96:108|256:108|256:192|256:276|416:276|464:196,1,480,2|0
|
||||
328,144,187125,2,0,B|296:316,1,160,2|0
|
||||
184,240,187625,2,0,B|216:68,1,160,2|0
|
||||
256,192,188125,1,6
|
||||
256,192,188250,12,2,189625
|
||||
464,188,190125,6,2,B|416:108|256:108|256:192|256:276|96:276|48:196,1,480,2|0
|
||||
184,144,191125,2,0,B|216:316,1,160,2|0
|
||||
328,240,191625,2,0,B|296:68,1,160,2|0
|
||||
164,32,192125,5,6
|
||||
28,84,192375,1,0
|
||||
28,228,192625,1,8
|
||||
128,332,192875,2,2,B|160:224|300:172|408:244,2,320,2|2|0
|
||||
276,356,194125,2,2,B|384:324|436:184|364:76,1,320
|
||||
236,28,194875,2,2,B|128:60|76:200|148:308,1,320,2|0
|
||||
280,268,195625,2,0,B|232:116,1,160,2|0
|
||||
104,52,196125,5,6
|
||||
136,192,196375,1,0
|
||||
116,344,196625,1,8
|
||||
256,312,196875,1,0
|
||||
332,312,197000,1,0
|
||||
408,332,197125,1,6
|
||||
392,264,197250,1,0
|
||||
376,192,197375,1,0
|
||||
396,40,197625,1,8
|
||||
256,72,197875,5,0
|
||||
256,72,198000,1,0
|
||||
256,72,198125,1,6
|
||||
136,192,198625,1,6
|
||||
256,312,199125,1,6
|
||||
376,192,199625,1,6
|
||||
256,192,200125,1,6
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
Mod = new CatchModHidden(),
|
||||
PassCondition = () => Player.Results.Count > 0
|
||||
&& Player.ChildrenOfType<DrawableJuiceStream>().Single().Alpha > 0
|
||||
&& Player.ChildrenOfType<DrawableFruit>().Last().Alpha > 0
|
||||
&& Player.ChildrenOfType<DrawableFruit>().First().Alpha > 0
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public partial class TestSceneOutOfBoundsObjects : TestSceneCatchPlayer
|
||||
{
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
[Test]
|
||||
public void TestNoOutOfBoundsObjects()
|
||||
{
|
||||
bool anyObjectOutOfBounds = false;
|
||||
|
||||
AddStep("reset flag", () => anyObjectOutOfBounds = false);
|
||||
|
||||
AddUntilStep("check for out-of-bounds objects",
|
||||
() =>
|
||||
{
|
||||
anyObjectOutOfBounds |= Player.ChildrenOfType<DrawableCatchHitObject>().Any(dho => dho.X < 0 || dho.X > CatchPlayfield.WIDTH);
|
||||
return Player.ScoreProcessor.HasCompleted.Value;
|
||||
});
|
||||
|
||||
AddAssert("no out of bound objects found", () => !anyObjectOutOfBounds);
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = ruleset,
|
||||
},
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Fruit { StartTime = 1000, X = -50 },
|
||||
new Fruit { StartTime = 1200, X = CatchPlayfield.WIDTH + 50 },
|
||||
new JuiceStream
|
||||
{
|
||||
StartTime = 1500,
|
||||
X = 10,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(-200, 0)
|
||||
})
|
||||
},
|
||||
new JuiceStream
|
||||
{
|
||||
StartTime = 3000,
|
||||
X = CatchPlayfield.WIDTH - 10,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(200, 0)
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Argon;
|
||||
@@ -235,5 +236,17 @@ namespace osu.Game.Rulesets.Catch
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor = new CatchScoreProcessor();
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int standardisedBonusScore;
|
||||
private int combo;
|
||||
@@ -60,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
@@ -75,6 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
attributes.MaxCombo = combo;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
@@ -133,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
private double placementStartTime;
|
||||
private double placementEndTime;
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
|
||||
public JuiceStreamPlacementBlueprint()
|
||||
{
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
@@ -189,6 +189,21 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
// The half of the height of the osu! playfield.
|
||||
public const float DEFAULT_LEGACY_CONVERT_Y = 192;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum preempt time at AR=10.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MIN = 450;
|
||||
|
||||
/// <summary>
|
||||
/// Median preempt time at AR=5.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MID = 1200;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum preempt time at AR=0.
|
||||
/// </summary>
|
||||
public const double PREEMPT_MAX = 1800;
|
||||
|
||||
/// <summary>
|
||||
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both });
|
||||
AddInternal(bananaContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, });
|
||||
AddInternal(dropletContainer = new NestedFruitContainer { RelativeSizeAxes = Axes.Both, });
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
|
||||
private void updateXPosition(ValueChangedEvent<float> _)
|
||||
{
|
||||
X = OriginalXBindable.Value + XOffsetBindable.Value;
|
||||
// same as `CatchHitObject.EffectiveX`.
|
||||
// not using that property directly to support scenarios where `HitObject` may not necessarily be present
|
||||
// for this pooled drawable.
|
||||
X = Math.Clamp(OriginalXBindable.Value + XOffsetBindable.Value, 0, CatchPlayfield.WIDTH);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
|
||||
@@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
public partial class NestedFruitContainer : Container
|
||||
{
|
||||
/// <remarks>
|
||||
/// This comparison logic is a copy of <see cref="HitObjectContainer"/> comparison logic,
|
||||
/// which can't be easily extracted to a more common place.
|
||||
/// </remarks>
|
||||
/// <seealso cref="HitObjectContainer.Compare"/>
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
if (x is not DrawableCatchHitObject xObj || y is not DrawableCatchHitObject yObj)
|
||||
return base.Compare(x, y);
|
||||
|
||||
int result = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
|
||||
return result == 0 ? CompareReverseChildID(x, y) : result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -103,8 +102,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
AddNested(new TinyDroplet
|
||||
{
|
||||
StartTime = t + lastEvent.Value.Time,
|
||||
X = ClampToPlayfield(EffectiveX + Path.PositionAt(
|
||||
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
|
||||
X = EffectiveX + Path.PositionAt(lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -121,7 +119,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Samples = dropletSamples,
|
||||
StartTime = e.Time,
|
||||
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
|
||||
X = EffectiveX + Path.PositionAt(e.PathProgress).X,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -132,16 +130,14 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
Samples = this.GetNodeSamples(nodeIndex++),
|
||||
StartTime = e.Time,
|
||||
X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
|
||||
X = EffectiveX + Path.PositionAt(e.PathProgress).X,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
|
||||
|
||||
public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
|
||||
public float EndX => EffectiveX + this.CurvePositionAt(1).X;
|
||||
|
||||
[JsonIgnore]
|
||||
public double Duration
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@@ -20,22 +21,75 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
private double fruitTinyScale;
|
||||
|
||||
public CatchScoreProcessor()
|
||||
: base(new CatchRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
// large ticks are *purposefully* not counted to match stable
|
||||
int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great);
|
||||
fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
}
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 600000 * comboProgress
|
||||
+ 400000 * Accuracy.Value * accuracyProgress
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
return comboPortion * comboProgress
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
// dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion
|
||||
case HitResult.Great:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SmallTickHit:
|
||||
return 300;
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
case HitResult.LargeBonus:
|
||||
return 200;
|
||||
}
|
||||
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
{
|
||||
double baseIncrease = 0;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Great:
|
||||
baseIncrease = 300;
|
||||
break;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
baseIncrease = 100;
|
||||
break;
|
||||
}
|
||||
|
||||
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
}
|
||||
|
||||
public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
|
||||
{
|
||||
if (accuracy == accuracy_cutoff_x)
|
||||
return ScoreRank.X;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -25,8 +26,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||
&& Player.ScoreProcessor.TotalScore.Value == 1_000_000,
|
||||
&& Precision.AlmostEquals(Player.ScoreProcessor.Accuracy.Value, 0.9836, 0.01)
|
||||
&& Player.ScoreProcessor.TotalScore.Value == 946_049,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
@@ -53,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
Mod = doubleTime,
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
|
||||
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneManiaModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
@@ -24,5 +29,52 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
EndTime = 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneManiaModSuddenDeath : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
public TestSceneManiaModSuddenDeath()
|
||||
: base(new ManiaModSuddenDeath())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBreakOnHoldNote() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
EndTime = 3000,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@@ -16,37 +14,35 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Test]
|
||||
public void TestMinor()
|
||||
{
|
||||
AddStep("Create barlines", () => recreate());
|
||||
AddStep("Create barlines", recreate);
|
||||
}
|
||||
|
||||
private void recreate(Func<IEnumerable<BarLine>>? createBarLines = null)
|
||||
private void recreate()
|
||||
{
|
||||
var stageDefinitions = new List<StageDefinition>
|
||||
{
|
||||
new StageDefinition(4),
|
||||
};
|
||||
|
||||
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
|
||||
SetContents(_ =>
|
||||
{
|
||||
if (createBarLines != null)
|
||||
var maniaPlayfield = new ManiaPlayfield(stageDefinitions);
|
||||
|
||||
// Must be scheduled so the pool is loaded before we try and retrieve from it.
|
||||
Schedule(() =>
|
||||
{
|
||||
var barLines = createBarLines();
|
||||
|
||||
foreach (var b in barLines)
|
||||
s.Add(b);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
s.Add(new BarLine
|
||||
for (int i = 0; i < 64; i++)
|
||||
{
|
||||
StartTime = Time.Current + i * 500,
|
||||
Major = i % 4 == 0,
|
||||
});
|
||||
}
|
||||
}));
|
||||
maniaPlayfield.Add(new BarLine
|
||||
{
|
||||
StartTime = Time.Current + i * 500,
|
||||
Major = i % 4 == 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return maniaPlayfield;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,12 +200,10 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||
assertComboAtJudgement(1, 1);
|
||||
assertComboAtJudgement(0, 1);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
assertComboAtJudgement(2, 0);
|
||||
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||
assertComboAtJudgement(4, 1);
|
||||
assertComboAtJudgement(1, 0);
|
||||
assertComboAtJudgement(3, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -59,23 +59,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
|
||||
{
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
}
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
||||
{
|
||||
return new LegacyScoreAttributes { ComboScore = 1000000 };
|
||||
return new LegacyScoreAttributes
|
||||
{
|
||||
ComboScore = 1000000,
|
||||
MaxCombo = 0 // Max combo is mod-dependent, so any value here is insufficient.
|
||||
};
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Duration > 0;
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
|
||||
|
||||
public HoldNotePlacementBlueprint()
|
||||
: base(new HoldNote())
|
||||
|
||||
@@ -10,5 +10,10 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
|
||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
||||
// make the map harder and is more of a personal preference.
|
||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,10 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
|
||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
||||
// make the map any harder and is more of a personal preference.
|
||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +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 osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModPerfect : ModPerfect
|
||||
{
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
||||
return false;
|
||||
|
||||
// Mania allows imperfect "Great" hits without failing.
|
||||
if (result.Judgement.MaxResult == HitResult.Perfect)
|
||||
return result.Type < HitResult.Great;
|
||||
|
||||
return result.Type != result.Judgement.MaxResult;
|
||||
}
|
||||
|
||||
private bool isRelevantResult(HitResult result) => result.AffectsAccuracy() || result.AffectsCombo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
@@ -40,8 +38,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
private Drawable headPiece;
|
||||
|
||||
private DrawableNotePerfectBonus perfectBonus;
|
||||
|
||||
public DrawableNote()
|
||||
: this(null)
|
||||
{
|
||||
@@ -93,10 +89,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
{
|
||||
perfectBonus.TriggerResult(false);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -107,16 +100,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
result = GetCappedResult(result);
|
||||
|
||||
perfectBonus.TriggerResult(result == HitResult.Perfect);
|
||||
ApplyResult(r => r.Type = result);
|
||||
}
|
||||
|
||||
public override void MissForcefully()
|
||||
{
|
||||
perfectBonus.TriggerResult(false);
|
||||
base.MissForcefully();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some objects in mania may want to limit the max result.
|
||||
/// </summary>
|
||||
@@ -137,32 +123,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableNotePerfectBonus bonus:
|
||||
AddInternal(perfectBonus = bonus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
RemoveInternal(perfectBonus, false);
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case NotePerfectBonus bonus:
|
||||
return new DrawableNotePerfectBonus(bonus);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
private void updateSnapColour()
|
||||
{
|
||||
if (beatmap == null || HitObject == null) return;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableNotePerfectBonus : DrawableManiaHitObject<NotePerfectBonus>
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableNotePerfectBonus()
|
||||
: this(null!)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a judgement result.
|
||||
/// </summary>
|
||||
/// <param name="hit">Whether this tick was reached.</param>
|
||||
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
|
||||
@@ -13,12 +12,5 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
public class Note : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new ManiaJudgement();
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
AddNested(new NotePerfectBonus { StartTime = StartTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class NotePerfectBonus : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public class NotePerfectBonusJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.SmallBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,37 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 10000 * comboProgress
|
||||
+ 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
|
||||
return 150000 * comboProgress
|
||||
+ 850000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
{
|
||||
return getBaseComboScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
}
|
||||
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return 305;
|
||||
}
|
||||
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
private int getBaseComboScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return 300;
|
||||
}
|
||||
|
||||
return GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
private class JudgementOrderComparer : IComparer<HitObject>
|
||||
{
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : JudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private const float judgement_y_position = 160;
|
||||
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
[Resolved]
|
||||
@@ -30,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
Y = 160;
|
||||
Y = judgement_y_position;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -76,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -100,16 +99,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(30));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
|
||||
|
||||
float width;
|
||||
|
||||
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
|
||||
|
||||
// Best effort until we have better mobile support.
|
||||
if (RuntimeInfo.IsMobile)
|
||||
width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
|
||||
else
|
||||
width = 60 * (isSpecialColumn ? 2 : 1);
|
||||
float width = 60 * (isSpecialColumn ? 2 : 1);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
FallbackColumnIndex = "S";
|
||||
else
|
||||
{
|
||||
int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index);
|
||||
// Account for cases like dual-stage (assume that all stages have the same column count for now).
|
||||
int columnInStage = Column.Index % stage.Columns;
|
||||
int distanceToEdge = Math.Min(columnInStage, (stage.Columns - 1) - columnInStage);
|
||||
FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
|
||||
|
||||
RegisterPool<Note, DrawableNote>(10, 50);
|
||||
RegisterPool<NotePerfectBonus, DrawableNotePerfectBonus>(10, 50);
|
||||
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
|
||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@@ -60,6 +63,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
onSkinChanged();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateMobileSizing();
|
||||
}
|
||||
|
||||
private void onSkinChanged()
|
||||
{
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
@@ -77,12 +86,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
?.Value;
|
||||
|
||||
if (width == null)
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
else
|
||||
columns[i].Width = width.Value;
|
||||
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
|
||||
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
|
||||
columns[i].Width = width.Value;
|
||||
}
|
||||
|
||||
updateMobileSizing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,10 +104,29 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <param name="content">The content.</param>
|
||||
public void SetContentForColumn(int column, TContent content) => columns[column].Child = content;
|
||||
|
||||
public new MarginPadding Padding
|
||||
private void updateMobileSizing()
|
||||
{
|
||||
get => base.Padding;
|
||||
set => base.Padding = value;
|
||||
if (!IsLoaded || !RuntimeInfo.IsMobile)
|
||||
return;
|
||||
|
||||
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
|
||||
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
|
||||
|
||||
// Will be null in tests.
|
||||
if (containingCell == null)
|
||||
return;
|
||||
|
||||
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
|
||||
|
||||
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
|
||||
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
|
||||
// 1.92 is a "reference" mobile screen aspect ratio for phones.
|
||||
// We should scale it back for cases like tablets which aren't so extreme.
|
||||
mobileAdjust *= aspectRatio / 1.92f;
|
||||
|
||||
// Best effort until we have better mobile support.
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
columns[i].Width *= mobileAdjust;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
||||
@@ -19,12 +19,14 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@@ -57,6 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@@ -104,7 +109,20 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
updateTimeRange();
|
||||
}
|
||||
|
||||
private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
|
||||
private void updateTimeRange()
|
||||
{
|
||||
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||
?? Stage.HIT_TARGET_POSITION;
|
||||
|
||||
const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION;
|
||||
float lengthToHitPosition = 768 - hitPosition;
|
||||
|
||||
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
|
||||
float scale = lengthToHitPosition / length_to_default_hit_position;
|
||||
|
||||
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
|
||||
|
||||
@@ -1,22 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly ColumnFlow<Column> columnFlow;
|
||||
|
||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
||||
private readonly JudgementPooler<DrawableManiaJudgement> judgementPooler;
|
||||
|
||||
private readonly Drawable barLineContainer;
|
||||
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly int firstColumnIndex;
|
||||
|
||||
private ISkinSource currentSkin = null!;
|
||||
|
||||
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
||||
{
|
||||
this.firstColumnIndex = firstColumnIndex;
|
||||
@@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
@@ -137,11 +139,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
var hitWindows = new ManiaHitWindows();
|
||||
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
@@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin != null)
|
||||
if (currentSkin.IsNotNull())
|
||||
currentSkin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
|
||||
@@ -196,13 +200,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
}));
|
||||
})!);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -1,8 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
@@ -21,5 +31,129 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
[Test]
|
||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||
|
||||
[Test]
|
||||
public void TestSliderDimsOnlyAfterStartTime()
|
||||
{
|
||||
bool sliderDimmedBeforeStartTime = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmedBeforeStartTime |=
|
||||
Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, },
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
StackLeniency = 0,
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(990, new Vector2()),
|
||||
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitEarly()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitLate()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneOsuModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
@@ -50,5 +54,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissSliderTail() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModPerfect(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModStrictTracking : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestSliderInput() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModStrictTracking(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(500, new Vector2(200, 0), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(501, new Vector2(200, 0)),
|
||||
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1751, new Vector2(0, 100)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModSuddenDeath : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
public TestSceneOsuModSuddenDeath()
|
||||
: base(new OsuModSuddenDeath())
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissTail() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestMissTick() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModSuddenDeath(),
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), })
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1000, new Vector2(256, 192), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1001, new Vector2(256, 192)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
public void TestUserAlreadyHasTouchDeviceActive()
|
||||
{
|
||||
loadPlayer();
|
||||
// it is presumed that a previous screen (i.e. song select) will set this up
|
||||
AddStep("set up touchscreen user", () =>
|
||||
{
|
||||
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
|
||||
@@ -69,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchActivePriorToPlayerLoad()
|
||||
{
|
||||
AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true));
|
||||
loadPlayer();
|
||||
AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchDuringBreak()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuRateAdjustedDisplayDifficultyTest
|
||||
{
|
||||
private static IEnumerable<float> difficultyValuesToTest()
|
||||
{
|
||||
for (float i = 0; i <= 10; i += 0.5f)
|
||||
yield return i;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestApproachRateIsUnchangedWithRateEqualToOne(float originalApproachRate)
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(difficultyValuesToTest))]
|
||||
public void TestOverallDifficultyIsUnchangedWithRateEqualToOne(float originalOverallDifficulty)
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateBelowOne()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRateAboveOne()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,16 +25,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly List<DrawablePool<TestDrawableOsuJudgement>> pools;
|
||||
private readonly List<DrawablePool<TestDrawableOsuJudgement>> pools = new List<DrawablePool<TestDrawableOsuJudgement>>();
|
||||
|
||||
public TestSceneDrawableJudgement()
|
||||
[TestCaseSource(nameof(validResults))]
|
||||
public void Test(HitResult result)
|
||||
{
|
||||
pools = new List<DrawablePool<TestDrawableOsuJudgement>>();
|
||||
|
||||
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Skip(1))
|
||||
showResult(result);
|
||||
showResult(result);
|
||||
}
|
||||
|
||||
private static IEnumerable<HitResult> validResults => Enum.GetValues<HitResult>().Skip(1);
|
||||
|
||||
[Test]
|
||||
public void TestHitLightingDisabled()
|
||||
{
|
||||
@@ -72,32 +72,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
pools.Add(pool = new DrawablePool<TestDrawableOsuJudgement>(1));
|
||||
else
|
||||
{
|
||||
pool = pools[poolIndex];
|
||||
|
||||
// We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
|
||||
pool = pools[poolIndex];
|
||||
((Container)pool.Parent!).Clear(false);
|
||||
}
|
||||
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
pool,
|
||||
pool.Get(j => j.Apply(new JudgementResult(new HitObject
|
||||
{
|
||||
StartTime = Time.Current
|
||||
}, new Judgement())
|
||||
{
|
||||
Type = result,
|
||||
}, null)).With(j =>
|
||||
{
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
})
|
||||
}
|
||||
Child = pool,
|
||||
};
|
||||
|
||||
// Must be scheduled so the pool is loaded before we try and retrieve from it.
|
||||
Schedule(() =>
|
||||
{
|
||||
container.Add(pool.Get(j => j.Apply(new JudgementResult(new HitObject
|
||||
{
|
||||
StartTime = Time.Current
|
||||
}, new Judgement())
|
||||
{
|
||||
Type = result,
|
||||
}, null)).With(j =>
|
||||
{
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
}));
|
||||
});
|
||||
|
||||
poolIndex++;
|
||||
return container;
|
||||
});
|
||||
|
||||
+5
-5
@@ -10,12 +10,12 @@ using osu.Game.Rulesets.Osu.Scoring;
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneOsuHealthProcessor
|
||||
public class TestSceneOsuLegacyHealthProcessor
|
||||
{
|
||||
[Test]
|
||||
public void TestNoBreak()
|
||||
{
|
||||
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
|
||||
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSingleBreak()
|
||||
{
|
||||
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
|
||||
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestOverlappingBreak()
|
||||
{
|
||||
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
|
||||
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSequentialBreak()
|
||||
{
|
||||
OsuHealthProcessor hp = new OsuHealthProcessor(-1000);
|
||||
OsuLegacyHealthProcessor hp = new OsuLegacyHealthProcessor(-1000);
|
||||
hp.ApplyBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
DrawableSlider dho = null;
|
||||
|
||||
AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider
|
||||
AddStep("create slider", () => Child = dho = new DrawableSlider(applyDefaults(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
IndexInCurrentCombo = 0,
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddWaitStep("wait for progression", 1);
|
||||
|
||||
AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider
|
||||
AddStep("apply new slider", () => dho.Apply(applyDefaults(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
ComboIndex = 1,
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Child = new SkinProvidingContainer(provider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = dho = new DrawableSlider(prepareObject(new Slider
|
||||
Child = dho = new DrawableSlider(applyDefaults(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
IndexInCurrentCombo = 0,
|
||||
@@ -97,7 +97,38 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("ball is red", () => dho.ChildrenOfType<LegacySliderBall>().Single().BallColour == Color4.Red);
|
||||
}
|
||||
|
||||
private Slider prepareObject(Slider slider)
|
||||
[Test]
|
||||
public void TestIncreaseRepeatCount()
|
||||
{
|
||||
DrawableSlider dho = null;
|
||||
|
||||
AddStep("create slider", () =>
|
||||
{
|
||||
Child = dho = new DrawableSlider(applyDefaults(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
IndexInCurrentCombo = 0,
|
||||
StartTime = Time.Current,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 100),
|
||||
new Vector2(300, 0),
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
AddStep("increase repeat count", () =>
|
||||
{
|
||||
dho.HitObject.RepeatCount++;
|
||||
applyDefaults(dho.HitObject);
|
||||
});
|
||||
|
||||
AddAssert("repeat got custom anchor", () =>
|
||||
dho.ChildrenOfType<DrawableSliderRepeat>().Single().RelativeAnchorPosition == Vector2.Divide(dho.SliderBody!.PathOffset, dho.DrawSize));
|
||||
}
|
||||
|
||||
private Slider applyDefaults(Slider slider)
|
||||
{
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
return slider;
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSliderEarlyHitJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double time_slider_start = 1000;
|
||||
private const double time_slider_end = 3000;
|
||||
|
||||
private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192);
|
||||
private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192);
|
||||
private static readonly Vector2 offset_inside_follow = new Vector2(35, 0);
|
||||
private static readonly Vector2 offset_outside_follow = offset_inside_follow * 2;
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private const float slider_path_length = 200;
|
||||
|
||||
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
[Test]
|
||||
public void TestHitEarlyMoveIntoFollowRegion()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEarlyAndReleaseInFollowRegion()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow),
|
||||
new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEarlyAndRepressInFollowRegion()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 75, slider_start_position + offset_inside_follow),
|
||||
new OsuReplayFrame(time_slider_start - 50, slider_start_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end - 50, slider_end_position + offset_inside_follow, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEarlyMoveOutsideFollowRegion()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start - 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start - 100, slider_start_position + offset_outside_follow, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end - 100, slider_end_position + offset_outside_follow, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check head result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTickJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check tick result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTick)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertRepeatJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check tick result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTailJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check tail result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertSliderJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
"check slider result",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private Vector2 computePositionFromTime(double time)
|
||||
{
|
||||
Vector2 dist = slider_end_position - slider_start_position;
|
||||
double t = (time - time_slider_start) / (time_slider_end - time_slider_start);
|
||||
return slider_start_position + dist * (float)t;
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Action<Slider>? adjustSliderFunc = null, bool classic = false)
|
||||
{
|
||||
Slider slider = new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(256 - slider_path_length / 2, 192),
|
||||
TickDistanceMultiplier = 3,
|
||||
ClassicSliderBehaviour = classic,
|
||||
Samples = new[]
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
},
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(slider_path_length, 0),
|
||||
}, slider_path_length),
|
||||
};
|
||||
|
||||
adjustSliderFunc?.Invoke(slider);
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = { slider },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderMultiplier = 1,
|
||||
SliderTickRate = 3,
|
||||
OverallDifficulty = 0
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
}
|
||||
});
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults.Clear();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -467,13 +467,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private void assertHeadMissTailTracked()
|
||||
{
|
||||
AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
|
||||
AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit));
|
||||
AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False);
|
||||
}
|
||||
|
||||
private void assertMidSliderJudgements()
|
||||
{
|
||||
AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
|
||||
AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.SliderTailHit));
|
||||
}
|
||||
|
||||
private void assertMidSliderJudgementFail()
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSliderLateHitJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
// Note: In the following tests, the terminology "in range of the follow circle" is used as meaning
|
||||
// the equivalent of "in range of the follow circle as if it were in its expanded state".
|
||||
|
||||
private const double time_slider_start = 1000;
|
||||
private const double time_slider_end = 1500;
|
||||
|
||||
private static readonly Vector2 slider_start_position = new Vector2(256 - slider_path_length / 2, 192);
|
||||
private static readonly Vector2 slider_end_position = new Vector2(256 + slider_path_length / 2, 192);
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private const float slider_path_length = 200;
|
||||
|
||||
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then tracking should be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeTracks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is NOT in range of the follow circle,
|
||||
/// then tracking should NOT be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateOutOfRangeDoesNotTrack()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is in range of the follow circle,
|
||||
/// then all ticks that the follow circle has passed through should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeHitsTicks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickHit);
|
||||
assertTickJudgement(1, HitResult.LargeTickHit);
|
||||
assertTickJudgement(2, HitResult.LargeTickHit);
|
||||
assertTickJudgement(3, HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is NOT in range of the follow circle,
|
||||
/// then all ticks that the follow circle has passed through should NOT be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateOutOfRangeDoesNotHitTicks()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is pressed after it's missed and the mouse is in range of the follow circle,
|
||||
/// then tracking should NOT be enabled.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestMissHeadInRangeDoesNotTrack()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 151, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 151, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(2, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(3, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late but after the completion of the slider and the mouse is in range of the follow circle,
|
||||
/// then all nested objects (ticks/repeats/tail) should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateShortSliderHitsAll()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(20, 0),
|
||||
}, 20);
|
||||
|
||||
s.TickDistanceMultiplier = 0.01f;
|
||||
s.RepeatCount = 1;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertAllTickJudgements(HitResult.LargeTickHit);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit late and the mouse is in range of the follow circle,
|
||||
/// then all the repeats that the follow circle has passed through should be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeHitsRepeat()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 0),
|
||||
}, 50);
|
||||
|
||||
s.RepeatCount = 1;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then only the ticks that are in range of the cursor position should be hit.
|
||||
/// If any hitobject does not meet this criteria, ALL hitobjects after that one should be missed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateDoesNotHitTicksIfAnyOutOfRange()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(70, 70),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.03f;
|
||||
s.SliderVelocityMultiplier = 6f;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
|
||||
// At least one tick was out of range, so they all should be missed.
|
||||
assertAllTickJudgements(HitResult.LargeTickMiss);
|
||||
|
||||
// This particular test actually starts tracking the slider just before the end, so the tail should be hit because of its leniency.
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then a tick not within the follow radius from the cursor position should not be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeDoesNotHitOutOfRangeTick()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 50),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.3f;
|
||||
s.SliderVelocityMultiplier = 3;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="TestHitLateInRangeDoesNotHitOutOfRangeTick"/> except the tracking is limited to the ball
|
||||
/// because the tick was missed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateInRangeDoesNotHitOutOfRangeTickAndTrackingLimitedToBall()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 50),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.25f;
|
||||
s.SliderVelocityMultiplier = 3;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the head circle is hit and the mouse is in range of the follow circle,
|
||||
/// then a tick not within the follow radius from the cursor position should not be hit.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitLateWithEdgeHit()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, 50),
|
||||
new Vector2(20, 0),
|
||||
});
|
||||
|
||||
s.TickDistanceMultiplier = 0.35f;
|
||||
s.SliderVelocityMultiplier = 4;
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.IgnoreMiss);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Late hit and release on each slider head of a slider stream.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestLateHitSliderStream()
|
||||
{
|
||||
var beatmap = new Beatmap<OsuHitObject>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
beatmap.HitObjects.Add(new Slider
|
||||
{
|
||||
StartTime = time_slider_start + 75 * i, // 200BPM @ 1/4
|
||||
Position = new Vector2(256 - slider_path_length / 2, 192),
|
||||
TickDistanceMultiplier = 3,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(20, 0),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
var replay = new List<ReplayFrame>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 75, slider_start_position, i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton));
|
||||
replay.Add(new OsuReplayFrame(time_slider_start + 75 * i + 140, slider_start_position));
|
||||
}
|
||||
|
||||
performTest(replay, beatmap);
|
||||
|
||||
AddAssert(
|
||||
$"all heads = {HitResult.Ok}",
|
||||
() => judgementResults.Where(r => r.HitObject is SliderHeadCircle).Select(r => r.Type),
|
||||
() => Has.All.EqualTo(HitResult.Ok));
|
||||
}
|
||||
|
||||
private void assertHeadJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"head = {result}",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderHeadCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTickJudgement(int index, HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"tick({index}) = {result}",
|
||||
() => judgementResults.Where(r => r.HitObject is SliderTick).ElementAtOrDefault(index)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertAllTickJudgements(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"all ticks = {result}",
|
||||
() => judgementResults.Where(r => r.HitObject is SliderTick).Select(t => t.Type),
|
||||
() => Has.All.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertRepeatJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"repeat = {result}",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderRepeat)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertTailJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"tail = {result}",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is SliderTailCircle)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void assertSliderJudgement(HitResult result)
|
||||
{
|
||||
AddAssert(
|
||||
$"slider = {result}",
|
||||
() => judgementResults.SingleOrDefault(r => r.HitObject is Slider)?.Type,
|
||||
() => Is.EqualTo(result));
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Action<Slider>? adjustSliderFunc = null, bool classic = false)
|
||||
{
|
||||
Slider slider = new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(256 - slider_path_length / 2, 192),
|
||||
TickDistanceMultiplier = 3,
|
||||
ClassicSliderBehaviour = classic,
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(slider_path_length, 0),
|
||||
}, slider_path_length),
|
||||
};
|
||||
|
||||
adjustSliderFunc?.Invoke(slider);
|
||||
|
||||
var beatmap = new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = { slider },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderMultiplier = 4,
|
||||
SliderTickRate = 3
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
performTest(frames, beatmap);
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
|
||||
{
|
||||
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
|
||||
beatmap.BeatmapInfo.StackLeniency = 0;
|
||||
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderMultiplier = 4,
|
||||
SliderTickRate = 3,
|
||||
};
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p)
|
||||
judgementResults.Add(result);
|
||||
|
||||
DrawableHitObject drawableObj = this.ChildrenOfType<DrawableHitObject>().Single(h => h.HitObject == result.HitObject);
|
||||
|
||||
var text = new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Position = Content.ToLocalSpace(drawableObj.ToScreenSpace(drawableObj.OriginPosition)) - new Vector2(0, 20),
|
||||
Text = result.IsHit ? "hit" : "miss"
|
||||
};
|
||||
|
||||
Add(text);
|
||||
|
||||
text.FadeOutFromOne(1000).Expire();
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults.Clear();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double trackerRotationTolerance = 0;
|
||||
|
||||
addSeekStep(5000);
|
||||
AddStep("calculate rotation tolerance", () =>
|
||||
{
|
||||
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
|
||||
});
|
||||
AddStep("calculate rotation tolerance", () => { trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); });
|
||||
AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100));
|
||||
|
||||
@@ -133,9 +130,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddAssert("player score matching expected bonus score", () =>
|
||||
{
|
||||
var scoreProcessor = ((ScoreExposedPlayer)Player).ScoreProcessor;
|
||||
|
||||
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
|
||||
long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
|
||||
long totalScore = scoreProcessor.TotalScore.Value * 2;
|
||||
return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * scoreProcessor.GetBaseScoreForResult(new SpinnerTick().CreateJudgement().MaxResult);
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
||||
@@ -5,12 +5,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
@@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor();
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int standardisedBonusScore;
|
||||
private int combo;
|
||||
@@ -60,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
@@ -171,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
base.LoadComplete();
|
||||
|
||||
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
|
||||
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
|
||||
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => updateConnectingPath());
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
updateConnectingPath();
|
||||
}
|
||||
|
||||
@@ -4,20 +4,15 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -41,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<DragEvent> DragInProgress;
|
||||
public Action DragEnded;
|
||||
|
||||
public List<PathControlPoint> PointsInSegment;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
@@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> hitObjectVersion;
|
||||
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
|
||||
cachePoints(hitObject);
|
||||
|
||||
hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the hit object path type for batch operations.
|
||||
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(hitObject);
|
||||
updatePathType();
|
||||
}));
|
||||
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
@@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
|
||||
|
||||
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
private void updatePathType()
|
||||
{
|
||||
if (ControlPoint.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (PointsInSegment.Count > 3)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
|
||||
if (PointsInSegment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
|
||||
+54
-3
@@ -14,10 +14,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -76,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
public void EnsureValidPathTypes()
|
||||
{
|
||||
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
|
||||
|
||||
foreach (var controlPoint in controlPoints)
|
||||
{
|
||||
if (controlPoint.Type != null)
|
||||
{
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
pointsInCurrentSegment.Clear();
|
||||
}
|
||||
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
}
|
||||
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
}
|
||||
|
||||
private void ensureValidPathType(IReadOnlyList<PathControlPoint> segment)
|
||||
{
|
||||
if (segment.Count == 0)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (segment.Count > 3)
|
||||
first.Type = PathType.BEZIER;
|
||||
|
||||
if (segment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
first.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
|
||||
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
|
||||
@@ -240,7 +286,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
{
|
||||
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
{
|
||||
@@ -249,8 +296,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
@@ -339,6 +386,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
|
||||
EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
public void DragEnded() => changeHandler?.EndChange();
|
||||
@@ -412,6 +461,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
});
|
||||
|
||||
if (countOfState == totalCount)
|
||||
|
||||
@@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
segmentStart.Type = PathType.BEZIER;
|
||||
break;
|
||||
}
|
||||
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
private void updateCursor()
|
||||
|
||||
@@ -254,6 +254,8 @@ 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, pathControlPoint);
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
return pathControlPoint;
|
||||
@@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
controlPoints.Remove(c);
|
||||
}
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
// Snap the slider to the current beat divisor before checking length validity.
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons())
|
||||
.Concat(new[]
|
||||
{
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
|
||||
});
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
@@ -1,14 +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 System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModAccuracyChallenge : ModAccuracyChallenge
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModAutopilot : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override string Name => "Autopilot";
|
||||
public override string Acronym => "AP";
|
||||
@@ -29,18 +29,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
typeof(OsuModSpunOut),
|
||||
typeof(ModRelax),
|
||||
typeof(ModFailCondition),
|
||||
typeof(ModNoFail),
|
||||
typeof(ModAutoplay),
|
||||
typeof(OsuModMagnetised),
|
||||
typeof(OsuModRepel),
|
||||
typeof(ModTouchDevice)
|
||||
};
|
||||
|
||||
public bool PerformFail() => false;
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
|
||||
private OsuInputManager inputManager = null!;
|
||||
|
||||
private List<OsuReplayFrame> replayFrames = null!;
|
||||
|
||||
@@ -18,7 +18,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableHealthProcessor
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
|
||||
|
||||
@@ -34,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
|
||||
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
|
||||
|
||||
[SettingSource("Classic health", "More closely resembles the original HP drain mechanics.")]
|
||||
public Bindable<bool> ClassicHealth { get; } = new Bindable<bool>(true);
|
||||
|
||||
private bool usingHiddenFading;
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
@@ -115,5 +118,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public HealthProcessor? CreateHealthProcessor(double drainStartTime) => ClassicHealth.Value ? new OsuLegacyHealthProcessor(drainStartTime) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override string Name => "Depth";
|
||||
public override string Acronym => "DP";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cube;
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "3D. Almost.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();
|
||||
|
||||
private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200);
|
||||
private readonly float sliderMinDepth = depthForScale(1.5f); // Depth at which slider's scale will be 1.5f
|
||||
|
||||
[SettingSource("Maximum depth", "How far away objects appear.", 0)]
|
||||
public BindableFloat MaxDepth { get; } = new BindableFloat(100)
|
||||
{
|
||||
Precision = 10,
|
||||
MinValue = 50,
|
||||
MaxValue = 200
|
||||
};
|
||||
|
||||
[SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)]
|
||||
public BindableBool ShowApproachCircles { get; } = new BindableBool(true);
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// Hide judgment displays and follow points as they won't make any sense.
|
||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
||||
}
|
||||
|
||||
private void applyTransform(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
if (!ShowApproachCircles.Value)
|
||||
{
|
||||
var hitObject = (OsuHitObject)drawable.HitObject;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
|
||||
using (circle.BeginAbsoluteSequence(appearTime))
|
||||
circle.ApproachCircle.Hide();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
double time = playfield.Time.Current;
|
||||
|
||||
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
|
||||
{
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
processHitObject(time, circle);
|
||||
break;
|
||||
|
||||
case DrawableSlider slider:
|
||||
processSlider(time, slider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processHitObject(double time, DrawableOsuHitObject drawable)
|
||||
{
|
||||
var hitObject = drawable.HitObject;
|
||||
|
||||
// Circles are always moving at the constant speed. They'll fade out before reaching the camera even at extreme conditions (AR 11, max depth).
|
||||
double speed = MaxDepth.Value / hitObject.TimePreempt;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed);
|
||||
|
||||
float scale = scaleForDepth(z);
|
||||
drawable.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
|
||||
drawable.Scale = new Vector2(scale);
|
||||
}
|
||||
|
||||
private void processSlider(double time, DrawableSlider drawableSlider)
|
||||
{
|
||||
var hitObject = drawableSlider.HitObject;
|
||||
|
||||
double baseSpeed = MaxDepth.Value / hitObject.TimePreempt;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
|
||||
// Allow slider to move at a constant speed if its scale at the end time will be lower than 1.5f
|
||||
float zEnd = MaxDepth.Value - (float)((Math.Max(hitObject.StartTime + hitObject.Duration, appearTime) - appearTime) * baseSpeed);
|
||||
|
||||
if (zEnd > sliderMinDepth)
|
||||
{
|
||||
processHitObject(time, drawableSlider);
|
||||
return;
|
||||
}
|
||||
|
||||
double offsetAfterStartTime = hitObject.Duration + 500;
|
||||
double slowSpeed = Math.Min(-sliderMinDepth / offsetAfterStartTime, baseSpeed);
|
||||
|
||||
double decelerationTime = hitObject.TimePreempt * 0.2;
|
||||
float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5);
|
||||
|
||||
float z;
|
||||
|
||||
if (time < hitObject.StartTime - decelerationTime)
|
||||
{
|
||||
float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime));
|
||||
z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed);
|
||||
}
|
||||
else if (time < hitObject.StartTime)
|
||||
{
|
||||
double timeOffset = time - (hitObject.StartTime - decelerationTime);
|
||||
double deceleration = (slowSpeed - baseSpeed) / decelerationTime;
|
||||
z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
double endTime = hitObject.StartTime + offsetAfterStartTime;
|
||||
z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed);
|
||||
}
|
||||
|
||||
float scale = scaleForDepth(z);
|
||||
drawableSlider.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
|
||||
drawableSlider.Scale = new Vector2(scale);
|
||||
}
|
||||
|
||||
private static float scaleForDepth(float depth) => -camera_position.Z / Math.Max(1f, depth - camera_position.Z);
|
||||
|
||||
private static float depthForScale(float scale) => -camera_position.Z / scale + camera_position.Z;
|
||||
|
||||
private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth)
|
||||
{
|
||||
return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
if (drawable is DrawableSlider s)
|
||||
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
|
||||
s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s);
|
||||
}
|
||||
|
||||
private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
|
||||
@@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||
public void OnSliderTrackingChange(DrawableSlider e)
|
||||
{
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
FlashlightDim = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||
|
||||
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray();
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) };
|
||||
|
||||
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
|
||||
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
||||
|
||||
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
||||
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
|
||||
|
||||
@@ -1,14 +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 System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModNoFail : ModNoFail
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected virtual float EndScale => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModDepth) };
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
|
||||
@@ -1,14 +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 System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModPerfect : ModPerfect
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
if (!slider.HeadCircle.IsHit)
|
||||
handleHitCircle(slider.HeadCircle);
|
||||
|
||||
requiresHold |= slider.Ball.IsHovered || h.IsHovered;
|
||||
requiresHold |= slider.SliderInputManager.IsMouseInFollowArea(true);
|
||||
break;
|
||||
|
||||
case DrawableSpinner spinner:
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "Hit objects run away!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
||||
|
||||
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
|
||||
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
|
||||
// further implementation will be required for supporting that.
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModDepth) };
|
||||
|
||||
private const int rotate_offset = 360;
|
||||
private const float rotate_starting_width = 2;
|
||||
|
||||
@@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (e.NewValue || slider.Judged) return;
|
||||
|
||||
if (slider.Time.Current < slider.HitObject.StartTime)
|
||||
return;
|
||||
|
||||
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
|
||||
|
||||
if (!tail.Judged)
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
typeof(OsuModAutopilot),
|
||||
typeof(OsuModTargetPractice),
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
typeof(OsuModRandom),
|
||||
typeof(OsuModSpunOut),
|
||||
typeof(OsuModStrictTracking),
|
||||
typeof(OsuModSuddenDeath)
|
||||
typeof(OsuModSuddenDeath),
|
||||
typeof(OsuModDepth)
|
||||
}).ToArray();
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(OsuModDepth) }).ToArray();
|
||||
|
||||
private float theta;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "They just won't stay still...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModDepth) };
|
||||
|
||||
private const int wiggle_duration = 100; // (ms) Higher = fewer wiggles
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user