mirror of
https://github.com/ppy/osu.git
synced 2026-05-21 05:49:54 +08:00
Compare commits
570 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
@@ -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="2023.1227.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,11 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// 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;
|
||||
|
||||
// run Squirrel first, as the app may exit after these run
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,7 +7,7 @@ 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.Types;
|
||||
@@ -18,6 +18,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;
|
||||
@@ -75,6 +77,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 +136,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
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
}
|
||||
|
||||
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));
|
||||
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 =
|
||||
@@ -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);
|
||||
|
||||
@@ -5,12 +5,12 @@ 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.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 +18,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;
|
||||
@@ -171,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
standardisedBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
||||
}
|
||||
else
|
||||
attributes.AccuracyScore += scoreIncrease;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
|
||||
{
|
||||
public OsuAction? HitAction => HitArea.HitAction;
|
||||
public OsuAction? HitAction => HitArea?.HitAction;
|
||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||
|
||||
public SkinnableDrawable ApproachCircle { get; private set; }
|
||||
|
||||
@@ -97,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public virtual void Shake() { }
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get hit, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
public void HitForcefully() => ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public IBindable<int> PathVersion => pathVersion;
|
||||
private readonly Bindable<int> pathVersion = new Bindable<int>();
|
||||
|
||||
public readonly SliderInputManager SliderInputManager;
|
||||
|
||||
private Container<DrawableSliderHead> headContainer;
|
||||
private Container<DrawableSliderTail> tailContainer;
|
||||
private Container<DrawableSliderTick> tickContainer;
|
||||
@@ -72,9 +74,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public DrawableSlider([CanBeNull] Slider s = null)
|
||||
: base(s)
|
||||
{
|
||||
SliderInputManager = new SliderInputManager(this);
|
||||
|
||||
Ball = new DrawableSliderBall
|
||||
{
|
||||
GetInitialHitAction = () => HeadCircle.HitAction,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
@@ -88,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
SliderInputManager,
|
||||
shakeContainer = new ShakeContainer
|
||||
{
|
||||
ShakeDuration = 30,
|
||||
@@ -124,8 +128,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
foreach (var drawableHitObject in NestedHitObjects)
|
||||
drawableHitObject.AccentColour.Value = colour.NewValue;
|
||||
}, true);
|
||||
|
||||
Tracking.BindValueChanged(updateSlidingSample);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
@@ -162,14 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
slidingSample?.Stop();
|
||||
}
|
||||
|
||||
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
slidingSample?.Play();
|
||||
else
|
||||
slidingSample?.Stop();
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
base.AddNestedHitObject(hitObject);
|
||||
@@ -232,11 +226,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Tracking.Value = Ball.Tracking;
|
||||
Tracking.Value = SliderInputManager.Tracking;
|
||||
|
||||
if (Tracking.Value && slidingSample != null)
|
||||
// keep the sliding sample playing at the current tracking position
|
||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||
if (slidingSample != null)
|
||||
{
|
||||
if (Tracking.Value && Time.Current >= HitObject.StartTime)
|
||||
{
|
||||
// keep the sliding sample playing at the current tracking position
|
||||
if (!slidingSample.IsPlaying)
|
||||
slidingSample.Play();
|
||||
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
|
||||
}
|
||||
else if (slidingSample.IsPlaying)
|
||||
slidingSample.Stop();
|
||||
}
|
||||
|
||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||
|
||||
@@ -245,8 +248,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
|
||||
if (hitObject is ITrackSnaking s)
|
||||
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
}
|
||||
|
||||
Size = SliderBody?.Size ?? Vector2.Zero;
|
||||
|
||||
@@ -4,14 +4,9 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
@@ -21,13 +16,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
|
||||
public partial class DrawableSliderBall : CircularContainer, ISliderProgress
|
||||
{
|
||||
public const float FOLLOW_AREA = 2.4f;
|
||||
|
||||
public Func<OsuAction?> GetInitialHitAction;
|
||||
|
||||
private Drawable followCircleReceptor;
|
||||
private DrawableSlider drawableSlider;
|
||||
private Drawable ball;
|
||||
|
||||
@@ -48,13 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
followCircleReceptor = new CircularContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true
|
||||
},
|
||||
ball = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -63,14 +48,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2? lastScreenSpaceMousePosition;
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
|
||||
{
|
||||
// Consider the case of rewinding - children's transforms are handled internally, so propagating down
|
||||
@@ -86,102 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.ApplyTransformsAt(time, false);
|
||||
}
|
||||
|
||||
private bool tracking;
|
||||
|
||||
public bool Tracking
|
||||
{
|
||||
get => tracking;
|
||||
private set
|
||||
{
|
||||
if (value == tracking)
|
||||
return;
|
||||
|
||||
tracking = value;
|
||||
|
||||
followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking.
|
||||
/// </summary>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
/// <summary>
|
||||
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
|
||||
///
|
||||
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
|
||||
/// Visually, this special case can be seen below (time increasing from left to right):
|
||||
///
|
||||
/// Z Z+X Z
|
||||
/// o========o
|
||||
///
|
||||
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
|
||||
///
|
||||
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
|
||||
///
|
||||
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
|
||||
/// </summary>
|
||||
private double? timeToAcceptAnyKeyAfter;
|
||||
|
||||
/// <summary>
|
||||
/// The actions that were pressed in the previous frame.
|
||||
/// </summary>
|
||||
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// from the point at which the head circle is hit, this will be non-null.
|
||||
// it may be null if the head circle was missed.
|
||||
var headCircleHitAction = GetInitialHitAction();
|
||||
|
||||
if (headCircleHitAction == null)
|
||||
timeToAcceptAnyKeyAfter = null;
|
||||
|
||||
var actions = drawableSlider.OsuActionInputManager?.PressedActions;
|
||||
|
||||
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
|
||||
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
|
||||
{
|
||||
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
|
||||
|
||||
// we can start accepting any key once all other keys have been released in the previous frame.
|
||||
if (!lastPressedActions.Contains(otherKey))
|
||||
timeToAcceptAnyKeyAfter = Time.Current;
|
||||
}
|
||||
|
||||
Tracking =
|
||||
// in valid time range
|
||||
Time.Current >= drawableSlider.HitObject.StartTime
|
||||
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
|
||||
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
|
||||
&& (!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime())
|
||||
// in valid position range
|
||||
&& lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
|
||||
// valid action
|
||||
(actions?.Any(isValidTrackingAction) ?? false);
|
||||
|
||||
lastPressedActions.Clear();
|
||||
if (actions != null)
|
||||
lastPressedActions.AddRange(actions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a given user input is a valid tracking action.
|
||||
/// </summary>
|
||||
private bool isValidTrackingAction(OsuAction action)
|
||||
{
|
||||
bool headCircleHit = GetInitialHitAction().HasValue;
|
||||
|
||||
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
|
||||
if (headCircleHit && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
|
||||
return action == GetInitialHitAction();
|
||||
|
||||
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
|
||||
}
|
||||
|
||||
private Vector2? lastPosition;
|
||||
|
||||
public void UpdateProgress(double completionProgress)
|
||||
|
||||
@@ -61,6 +61,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
CheckHittable = (d, t, r) => DrawableSlider.CheckHittable?.Invoke(d, t, r) ?? ClickAction.Hit;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
DrawableSlider.SliderInputManager.PostProcessHeadJudgement(this);
|
||||
}
|
||||
|
||||
protected override HitResult ResultFor(double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject != null);
|
||||
|
||||
@@ -17,7 +17,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
|
||||
{
|
||||
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
||||
|
||||
@@ -34,10 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private Drawable scaleContainer;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
public DrawableSliderRepeat()
|
||||
: base(null)
|
||||
{
|
||||
@@ -85,21 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Position = HitObject.Position - DrawableSlider.Position;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
// shared implementation with DrawableSliderTick.
|
||||
if (timeOffset >= 0)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
|
||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Skinning;
|
||||
@@ -17,7 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
|
||||
public partial class DrawableSliderTail : DrawableOsuHitObject
|
||||
{
|
||||
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
|
||||
|
||||
@@ -26,19 +24,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
|
||||
/// </summary>
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hit samples only play on successful hits.
|
||||
/// If <c>false</c>, the hit samples will also play on misses.
|
||||
/// </summary>
|
||||
public bool SamplePlaysOnlyOnHit { get; set; } = true;
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
public SkinnableDrawable CirclePiece { get; private set; }
|
||||
|
||||
private Container scaleContainer;
|
||||
@@ -125,36 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (userTriggered)
|
||||
return;
|
||||
|
||||
// Ensure the tail can only activate after all previous ticks/repeats already have.
|
||||
//
|
||||
// This covers the edge case where the lenience may allow the tail to activate before
|
||||
// the last tick, changing ordering of score/combo awarding.
|
||||
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
|
||||
if (lastTick?.Judged == false)
|
||||
return;
|
||||
|
||||
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
|
||||
return;
|
||||
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
|
||||
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
|
||||
// An actual tick miss should only occur if reaching the tick itself.
|
||||
if (Tracking)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
else if (timeOffset > 0)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
|
||||
@@ -14,16 +14,12 @@ using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking
|
||||
public partial class DrawableSliderTick : DrawableOsuHitObject
|
||||
{
|
||||
public const double ANIM_DURATION = 150;
|
||||
|
||||
private const float default_tick_size = 16;
|
||||
|
||||
public bool Tracking { get; set; }
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
private SkinnableDrawable scaleContainer;
|
||||
@@ -73,21 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Position = HitObject.Position - DrawableSlider.HitObject.Position;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
// shared implementation with DrawableSliderRepeat.
|
||||
if (timeOffset >= 0)
|
||||
{
|
||||
// Attempt to preserve correct ordering of judgements as best we can by forcing
|
||||
// an un-judged head to be missed when the user has clearly skipped it.
|
||||
//
|
||||
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
|
||||
if (Tracking && !DrawableSlider.HeadCircle.Judged)
|
||||
DrawableSlider.HeadCircle.MissForcefully();
|
||||
|
||||
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
@@ -107,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
case ArmedState.Miss:
|
||||
this.FadeOut(ANIM_DURATION);
|
||||
this.FadeColour(Color4.Red, ANIM_DURATION / 2);
|
||||
this.TransformBindableTo(AccentColour, Color4.Red, 0);
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
private const float spinning_sample_initial_frequency = 1.0f;
|
||||
private const float spinning_sample_modulated_base_frequency = 0.5f;
|
||||
|
||||
private SkinnableSound maxBonusSample;
|
||||
private PausableSkinnableSound maxBonusSample;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
|
||||
@@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Looping = true,
|
||||
Frequency = { Value = spinning_sample_initial_frequency }
|
||||
},
|
||||
maxBonusSample = new SkinnableSound
|
||||
maxBonusSample = new PausableSkinnableSound
|
||||
{
|
||||
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||
}
|
||||
@@ -312,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
private static readonly int score_per_tick = new OsuScoreProcessor().GetBaseScoreForResult(new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxResult);
|
||||
|
||||
private void updateBonusScore()
|
||||
{
|
||||
|
||||
@@ -1,13 +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.Osu.Objects.Drawables
|
||||
{
|
||||
public interface IRequireTracking
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the <see cref="DrawableSlider"/> is currently being tracked by the user.
|
||||
/// </summary>
|
||||
bool Tracking { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class SliderInputManager : Component, IRequireHighFrequencyMousePosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the slider is currently being tracked.
|
||||
/// </summary>
|
||||
public bool Tracking { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
|
||||
///
|
||||
/// This is a requirement to stop the case where a player holds down one key (from before the slider) and taps the second key while maintaining full scoring (tracking) of sliders.
|
||||
/// Visually, this special case can be seen below (time increasing from left to right):
|
||||
///
|
||||
/// Z Z+X Z
|
||||
/// o========o
|
||||
///
|
||||
/// Without this logic, tracking would continue through the entire slider even though no key hold action is directly attributing to it.
|
||||
///
|
||||
/// In all other cases, no special handling is required (either key being pressed is allowable as valid tracking).
|
||||
///
|
||||
/// The reason for storing this as a time value (rather than a bool) is to correctly handle rewind scenarios.
|
||||
/// </summary>
|
||||
private double? timeToAcceptAnyKeyAfter;
|
||||
|
||||
/// <summary>
|
||||
/// The actions that were pressed in the previous frame.
|
||||
/// </summary>
|
||||
private readonly List<OsuAction> lastPressedActions = new List<OsuAction>();
|
||||
|
||||
private Vector2? screenSpaceMousePosition;
|
||||
private readonly DrawableSlider slider;
|
||||
|
||||
public SliderInputManager(DrawableSlider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This component handles all input of the slider, so it should receive input no matter the position.
|
||||
/// </summary>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
screenSpaceMousePosition = e.ScreenSpaceMousePosition;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
updateTracking(IsMouseInFollowArea(Tracking));
|
||||
}
|
||||
|
||||
public void PostProcessHeadJudgement(DrawableSliderHead head)
|
||||
{
|
||||
if (!head.Judged || !head.Result.IsHit)
|
||||
return;
|
||||
|
||||
if (!IsMouseInFollowArea(true))
|
||||
return;
|
||||
|
||||
Debug.Assert(screenSpaceMousePosition != null);
|
||||
|
||||
Vector2 mousePositionInSlider = slider.ToLocalSpace(screenSpaceMousePosition.Value) - slider.OriginPosition;
|
||||
|
||||
// When the head is hit late:
|
||||
// - If the cursor has at all times been within range of the expanded follow area, hit all nested objects that have been passed through.
|
||||
// - If the cursor has at some point left the expanded follow area, miss those nested objects instead.
|
||||
|
||||
bool allTicksInRange = true;
|
||||
|
||||
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
// Skip nested objects that are already judged.
|
||||
if (nested.Judged)
|
||||
continue;
|
||||
|
||||
// Stop the process when a nested object is reached that can't be hit before the current time.
|
||||
if (nested.HitObject.StartTime > Time.Current)
|
||||
break;
|
||||
|
||||
float radius = getFollowRadius(true);
|
||||
double objectProgress = Math.Clamp((nested.HitObject.StartTime - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
|
||||
Vector2 objectPosition = slider.HitObject.CurvePositionAt(objectProgress);
|
||||
|
||||
// When the first nested object that is further outside the follow area is reached,
|
||||
// forcefully miss all other nested objects that would otherwise be valid to be hit.
|
||||
// This covers a case of a slider overlapping itself that requires tracking to a tick on an outer edge.
|
||||
if ((objectPosition - mousePositionInSlider).LengthSquared > radius * radius)
|
||||
{
|
||||
allTicksInRange = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var nested in slider.NestedHitObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
// Skip nested objects that are already judged.
|
||||
if (nested.Judged)
|
||||
continue;
|
||||
|
||||
// Stop the process when a nested object is reached that can't be hit before the current time.
|
||||
if (nested.HitObject.StartTime > Time.Current)
|
||||
break;
|
||||
|
||||
if (allTicksInRange)
|
||||
nested.HitForcefully();
|
||||
else
|
||||
nested.MissForcefully();
|
||||
}
|
||||
|
||||
// If all ticks were hit so far, enable tracking the full extent.
|
||||
// If any ticks were missed, assume tracking would've broken at some point, and should only activate if the cursor is within the slider ball.
|
||||
// For the second case, this may be the last chance we have to enable tracking before other objects get judged, otherwise the same would normally happen via Update().
|
||||
updateTracking(allTicksInRange || IsMouseInFollowArea(false));
|
||||
}
|
||||
|
||||
public void TryJudgeNestedObject(DrawableOsuHitObject nestedObject, double timeOffset)
|
||||
{
|
||||
switch (nestedObject)
|
||||
{
|
||||
case DrawableSliderRepeat:
|
||||
case DrawableSliderTick:
|
||||
if (timeOffset < 0)
|
||||
return;
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSliderTail:
|
||||
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
|
||||
return;
|
||||
|
||||
// Ensure the tail can only activate after all previous ticks/repeats already have.
|
||||
//
|
||||
// This covers the edge case where the lenience may allow the tail to activate before
|
||||
// the last tick, changing ordering of score/combo awarding.
|
||||
var lastTick = slider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
|
||||
if (lastTick?.Judged == false)
|
||||
return;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slider.HeadCircle.Judged)
|
||||
return;
|
||||
|
||||
if (Tracking)
|
||||
nestedObject.HitForcefully();
|
||||
else if (timeOffset >= 0)
|
||||
nestedObject.MissForcefully();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mouse is currently in the follow area.
|
||||
/// </summary>
|
||||
/// <param name="expanded">Whether to test against the maximum area of the follow circle.</param>
|
||||
public bool IsMouseInFollowArea(bool expanded)
|
||||
{
|
||||
if (screenSpaceMousePosition is not Vector2 pos)
|
||||
return false;
|
||||
|
||||
float radius = getFollowRadius(expanded);
|
||||
|
||||
double followProgress = Math.Clamp((Time.Current - slider.HitObject.StartTime) / slider.HitObject.Duration, 0, 1);
|
||||
Vector2 followCirclePosition = slider.HitObject.CurvePositionAt(followProgress);
|
||||
Vector2 mousePositionInSlider = slider.ToLocalSpace(pos) - slider.OriginPosition;
|
||||
|
||||
return (mousePositionInSlider - followCirclePosition).LengthSquared <= radius * radius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the radius of the follow area.
|
||||
/// </summary>
|
||||
/// <param name="expanded">Whether to return the maximum area of the follow circle.</param>
|
||||
private float getFollowRadius(bool expanded)
|
||||
{
|
||||
float radius = (float)slider.HitObject.Radius;
|
||||
|
||||
if (expanded)
|
||||
radius *= DrawableSliderBall.FOLLOW_AREA;
|
||||
|
||||
return radius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the tracking state.
|
||||
/// </summary>
|
||||
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
|
||||
private void updateTracking(bool isValidTrackingPosition)
|
||||
{
|
||||
// from the point at which the head circle is hit, this will be non-null.
|
||||
// it may be null if the head circle was missed.
|
||||
OsuAction? headCircleHitAction = getInitialHitAction();
|
||||
|
||||
if (headCircleHitAction == null)
|
||||
timeToAcceptAnyKeyAfter = null;
|
||||
|
||||
var actions = slider.OsuActionInputManager?.PressedActions;
|
||||
|
||||
// if the head circle was hit with a specific key, tracking should only occur while that key is pressed.
|
||||
if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null)
|
||||
{
|
||||
var otherKey = headCircleHitAction == OsuAction.RightButton ? OsuAction.LeftButton : OsuAction.RightButton;
|
||||
|
||||
// we can start accepting any key once all other keys have been released in the previous frame.
|
||||
if (!lastPressedActions.Contains(otherKey))
|
||||
timeToAcceptAnyKeyAfter = Time.Current;
|
||||
}
|
||||
|
||||
Tracking =
|
||||
// even in an edge case where current time has exceeded the slider's time, we may not have finished judging.
|
||||
// we don't want to potentially update from Tracking=true to Tracking=false at this point.
|
||||
(!slider.AllJudged || Time.Current <= slider.HitObject.GetEndTime())
|
||||
// in valid position range
|
||||
&& isValidTrackingPosition
|
||||
// valid action
|
||||
&& (actions?.Any(isValidTrackingAction) ?? false);
|
||||
|
||||
lastPressedActions.Clear();
|
||||
if (actions != null)
|
||||
lastPressedActions.AddRange(actions);
|
||||
}
|
||||
|
||||
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a given user input is a valid tracking action.
|
||||
/// </summary>
|
||||
private bool isValidTrackingAction(OsuAction action)
|
||||
{
|
||||
OsuAction? hitAction = getInitialHitAction();
|
||||
|
||||
// if the head circle was hit, we may not yet be allowed to accept any key, so we must use the initial hit action.
|
||||
if (hitAction.HasValue && (!timeToAcceptAnyKeyAfter.HasValue || Time.Current <= timeToAcceptAnyKeyAfter.Value))
|
||||
return action == hitAction;
|
||||
|
||||
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </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;
|
||||
|
||||
public double TimePreempt = 600;
|
||||
public double TimeFadeIn = 400;
|
||||
|
||||
@@ -148,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
|
||||
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public class TailJudgement : SliderEndJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.LargeTickHit;
|
||||
public override HitResult MaxResult => HitResult.SliderTailHit;
|
||||
public override HitResult MinResult => HitResult.IgnoreMiss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new ModAdaptiveSpeed(),
|
||||
new OsuModFreezeFrame(),
|
||||
new OsuModBubbles(),
|
||||
new OsuModSynesthesia()
|
||||
new OsuModSynesthesia(),
|
||||
new OsuModDepth()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -276,6 +277,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SliderTailHit,
|
||||
HitResult.SmallBonus,
|
||||
HitResult.LargeBonus,
|
||||
};
|
||||
@@ -288,6 +290,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
case HitResult.LargeTickHit:
|
||||
return "slider tick";
|
||||
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.SmallTickHit:
|
||||
return "slider end";
|
||||
|
||||
@@ -331,5 +334,23 @@ namespace osu.Game.Rulesets.Osu
|
||||
}
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
|
||||
|
||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||
/// <seealso cref="OsuHitWindows"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
adjustedDifficulty.ApproachRate = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(preempt, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
|
||||
|
||||
var greatHitWindowRange = OsuHitWindows.OSU_RANGES.Single(range => range.Result == HitResult.Great);
|
||||
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
greatHitWindow /= rate;
|
||||
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(greatHitWindow, greatHitWindowRange.Min, greatHitWindowRange.Average, greatHitWindowRange.Max);
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,82 @@
|
||||
// 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 System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor
|
||||
public partial class OsuHealthProcessor : DrainingHealthProcessor
|
||||
{
|
||||
public OsuHealthProcessor(double drainStartTime)
|
||||
: base(drainStartTime)
|
||||
private ComboResult currentComboResult = ComboResult.Perfect;
|
||||
|
||||
public OsuHealthProcessor(double drainStartTime, double drainLenience = 0)
|
||||
: base(drainStartTime, drainLenience)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject)
|
||||
protected override double GetHealthIncreaseFor(JudgementResult result)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Slider slider:
|
||||
foreach (var nested in slider.NestedHitObjects)
|
||||
yield return nested;
|
||||
if (IsSimulating)
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
if (result.HitObject is not IHasComboInformation combo)
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
if (combo.NewCombo)
|
||||
currentComboResult = ComboResult.Perfect;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Ok:
|
||||
setComboResult(ComboResult.Good);
|
||||
break;
|
||||
|
||||
case Spinner spinner:
|
||||
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
|
||||
yield return nested;
|
||||
|
||||
case HitResult.Meh:
|
||||
case HitResult.Miss:
|
||||
setComboResult(ComboResult.None);
|
||||
break;
|
||||
}
|
||||
|
||||
// The slider tail has a special judgement that can't accurately be described above.
|
||||
if (result.HitObject is SliderTailCircle && !result.IsHit)
|
||||
setComboResult(ComboResult.Good);
|
||||
|
||||
if (combo.LastInCombo && result.Type.IsHit())
|
||||
{
|
||||
switch (currentComboResult)
|
||||
{
|
||||
case ComboResult.Perfect:
|
||||
return getHealthIncreaseFor(result) + 0.07;
|
||||
|
||||
case ComboResult.Good:
|
||||
return getHealthIncreaseFor(result) + 0.05;
|
||||
|
||||
default:
|
||||
return getHealthIncreaseFor(result) + 0.03;
|
||||
}
|
||||
}
|
||||
|
||||
return getHealthIncreaseFor(result);
|
||||
|
||||
void setComboResult(ComboResult comboResult) => currentComboResult = (ComboResult)Math.Min((int)currentComboResult, (int)comboResult);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
double increase = 0;
|
||||
base.Reset(storeResults);
|
||||
currentComboResult = ComboResult.Perfect;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
private double getHealthIncreaseFor(JudgementResult result)
|
||||
{
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
|
||||
@@ -53,37 +88,41 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
// This result always comes from the slider tail, which is judged the same as a repeat.
|
||||
increase = 0.02;
|
||||
break;
|
||||
// When classic slider mechanics are enabled, this result comes from the tail.
|
||||
return 0.02;
|
||||
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.LargeTickHit:
|
||||
// This result comes from either a slider tick or repeat.
|
||||
increase = hitObject is SliderTick ? 0.015 : 0.02;
|
||||
switch (result.HitObject)
|
||||
{
|
||||
case SliderTick:
|
||||
return 0.015;
|
||||
|
||||
case SliderHeadCircle:
|
||||
case SliderTailCircle:
|
||||
case SliderRepeat:
|
||||
return 0.02;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
increase = 0.002;
|
||||
break;
|
||||
return 0.002;
|
||||
|
||||
case HitResult.Ok:
|
||||
increase = 0.011;
|
||||
break;
|
||||
return 0.011;
|
||||
|
||||
case HitResult.Great:
|
||||
increase = 0.03;
|
||||
break;
|
||||
return 0.03;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
increase = 0.0085;
|
||||
break;
|
||||
return 0.0085;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
increase = 0.01;
|
||||
break;
|
||||
return 0.01;
|
||||
}
|
||||
|
||||
return HpMultiplierNormal * increase;
|
||||
return base.GetHealthIncreaseFor(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
/// </summary>
|
||||
public const double MISS_WINDOW = 400;
|
||||
|
||||
private static readonly DifficultyRange[] osu_ranges =
|
||||
internal static readonly DifficultyRange[] OSU_RANGES =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 80, 50, 20),
|
||||
new DifficultyRange(HitResult.Ok, 140, 100, 60),
|
||||
@@ -34,6 +34,6 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => osu_ranges;
|
||||
protected override DifficultyRange[] GetRanges() => OSU_RANGES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public partial class OsuLegacyHealthProcessor : LegacyDrainingHealthProcessor
|
||||
{
|
||||
public OsuLegacyHealthProcessor(double drainStartTime)
|
||||
: base(drainStartTime)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Slider slider:
|
||||
foreach (var nested in slider.NestedHitObjects)
|
||||
yield return nested;
|
||||
|
||||
break;
|
||||
|
||||
case Spinner spinner:
|
||||
foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick))
|
||||
yield return nested;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14);
|
||||
|
||||
case HitResult.Miss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
// This result always comes from the slider tail, which is judged the same as a repeat.
|
||||
increase = 0.02;
|
||||
break;
|
||||
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.LargeTickHit:
|
||||
// This result comes from either a slider tick or repeat.
|
||||
increase = hitObject is SliderTick ? 0.015 : 0.02;
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
increase = 0.002;
|
||||
break;
|
||||
|
||||
case HitResult.Ok:
|
||||
increase = 0.011;
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
increase = 0.03;
|
||||
break;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
increase = 0.0085;
|
||||
break;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
increase = 0.01;
|
||||
break;
|
||||
}
|
||||
|
||||
return HpMultiplierNormal * increase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,25 +62,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
switch (Result)
|
||||
if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss)
|
||||
{
|
||||
default:
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
break;
|
||||
this.RotateTo(-45);
|
||||
this.ScaleTo(1.8f);
|
||||
this.ScaleTo(1.2f, 100, Easing.In);
|
||||
|
||||
case HitResult.Miss:
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 10), 800, Easing.InQuint);
|
||||
}
|
||||
else if (Result.IsMiss())
|
||||
{
|
||||
this.ScaleTo(1.6f);
|
||||
this.ScaleTo(1, 100, Easing.In);
|
||||
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
this.MoveTo(Vector2.Zero);
|
||||
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
|
||||
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
break;
|
||||
this.RotateTo(0);
|
||||
this.RotateTo(40, 800, Easing.InQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
this.FadeOutFromOne(800);
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
GlowColour = new Color4(171, 255, 255, 255)
|
||||
GlowColour = new Color4(171, 255, 255, 180)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
if (Skin is ArgonProSkin && (resultComponent.Component == HitResult.Great || resultComponent.Component == HitResult.Perfect))
|
||||
return Drawable.Empty();
|
||||
|
||||
return new ArgonJudgementPiece(resultComponent.Component);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -26,13 +27,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(tracking =>
|
||||
{
|
||||
Debug.Assert(ParentObject != null);
|
||||
|
||||
if (ParentObject.Judged)
|
||||
return;
|
||||
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
using (BeginAbsoluteSequence(Math.Max(Time.Current, ParentObject.HitObject?.StartTime ?? 0)))
|
||||
{
|
||||
if (tracking.NewValue)
|
||||
OnSliderPress();
|
||||
else
|
||||
OnSliderRelease();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
decimal? legacyVersion = skin.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value;
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
if (legacyVersion > 1.0m)
|
||||
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
|
||||
hitCircleText.FadeOut(legacy_fade_duration / 4);
|
||||
else
|
||||
|
||||
@@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
};
|
||||
|
||||
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
|
||||
userCursorScale.ValueChanged += _ => calculateCursorScale();
|
||||
userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
|
||||
autoCursorScale.ValueChanged += _ => calculateCursorScale();
|
||||
autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
|
||||
}
|
||||
@@ -81,10 +81,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
calculateCursorScale();
|
||||
cursorScale.Value = CalculateCursorScale();
|
||||
}
|
||||
|
||||
private void calculateCursorScale()
|
||||
protected virtual float CalculateCursorScale()
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
|
||||
}
|
||||
|
||||
cursorScale.Value = scale;
|
||||
return scale;
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -66,8 +65,21 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
HitPolicy = new StartTimeOrderedHitPolicy();
|
||||
|
||||
var hitWindows = new OsuHitWindows();
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r =>
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case HitResult.Great:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.IgnoreMiss:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}))
|
||||
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
|
||||
|
||||
AddRangeInternal(poolDictionary.Values);
|
||||
@@ -170,7 +182,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
|
||||
if (!poolDictionary.TryGetValue(result.Type, out var pool))
|
||||
return;
|
||||
|
||||
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
|
||||
|
||||
judgementLayer.Add(explosion);
|
||||
|
||||
|
||||
@@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
RelativePositionAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override float CalculateCursorScale()
|
||||
{
|
||||
// Force minimum cursor size so it's easily clickable
|
||||
return Math.Max(1f, base.CalculateCursorScale());
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateColour();
|
||||
|
||||
@@ -10,7 +10,7 @@ using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModPerfect : ModPerfectTestScene
|
||||
public partial class TestSceneTaikoModPerfect : ModFailConditionTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user