mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 04:32:11 +08:00
Compare commits
579 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
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1219.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.114.0" />
|
||||
</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
|
||||
}
|
||||
}
|
||||
+18
-1
@@ -30,7 +30,19 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// run Squirrel first, as the app may exit after these run
|
||||
/*
|
||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
||||
*
|
||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
||||
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
||||
* namely by checking loaded assemblies:
|
||||
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
||||
*
|
||||
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
||||
* the app will then do completely broken things like:
|
||||
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
||||
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
||||
*/
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
@@ -54,6 +66,11 @@ namespace osu.Desktop
|
||||
setupSquirrel();
|
||||
}
|
||||
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
// Lazer and stable share the same executable name.
|
||||
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@@ -20,20 +21,73 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
private double fruitTinyScale;
|
||||
|
||||
public CatchScoreProcessor()
|
||||
: base(new CatchRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
// large ticks are *purposefully* not counted to match stable
|
||||
int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great);
|
||||
fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
}
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 600000 * comboProgress
|
||||
+ 400000 * Accuracy.Value * accuracyProgress
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
return comboPortion * comboProgress
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
// dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion
|
||||
case HitResult.Great:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SmallTickHit:
|
||||
return 300;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 200;
|
||||
}
|
||||
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
{
|
||||
double baseIncrease = 0;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Great:
|
||||
baseIncrease = 300;
|
||||
break;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
baseIncrease = 100;
|
||||
break;
|
||||
}
|
||||
|
||||
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
}
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,12 +19,14 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@@ -57,6 +59,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@@ -104,7 +109,20 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
updateTimeRange();
|
||||
}
|
||||
|
||||
private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
|
||||
private void updateTimeRange()
|
||||
{
|
||||
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||
?? Stage.HIT_TARGET_POSITION;
|
||||
|
||||
const float length_to_default_hit_position = 768 - LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION;
|
||||
float lengthToHitPosition = 768 - hitPosition;
|
||||
|
||||
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
|
||||
float scale = lengthToHitPosition / length_to_default_hit_position;
|
||||
|
||||
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly ColumnFlow<Column> columnFlow;
|
||||
|
||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
||||
private readonly JudgementPooler<DrawableManiaJudgement> judgementPooler;
|
||||
|
||||
private readonly Drawable barLineContainer;
|
||||
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly int firstColumnIndex;
|
||||
|
||||
private ISkinSource currentSkin = null!;
|
||||
|
||||
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
||||
{
|
||||
this.firstColumnIndex = firstColumnIndex;
|
||||
@@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
@@ -137,11 +139,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
var hitWindows = new ManiaHitWindows();
|
||||
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
@@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin != null)
|
||||
if (currentSkin.IsNotNull())
|
||||
currentSkin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
|
||||
@@ -196,13 +200,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
}));
|
||||
})!);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
@@ -21,5 +31,129 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
[Test]
|
||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||
|
||||
[Test]
|
||||
public void TestSliderDimsOnlyAfterStartTime()
|
||||
{
|
||||
bool sliderDimmedBeforeStartTime = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmedBeforeStartTime |=
|
||||
Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, },
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
StackLeniency = 0,
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(990, new Vector2()),
|
||||
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitEarly()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitLate()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
public void TestUserAlreadyHasTouchDeviceActive()
|
||||
{
|
||||
loadPlayer();
|
||||
// it is presumed that a previous screen (i.e. song select) will set this up
|
||||
AddStep("set up touchscreen user", () =>
|
||||
{
|
||||
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
|
||||
@@ -69,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchActivePriorToPlayerLoad()
|
||||
{
|
||||
AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true));
|
||||
loadPlayer();
|
||||
AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchDuringBreak()
|
||||
{
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertTickJudgement(1, HitResult.LargeTickHit);
|
||||
assertTickJudgement(2, HitResult.LargeTickHit);
|
||||
assertTickJudgement(3, HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertAllTickJudgements(HitResult.LargeTickHit);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertRepeatJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
@@ -276,7 +276,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertHeadJudgement(HitResult.Meh);
|
||||
assertTickJudgement(0, HitResult.LargeTickMiss);
|
||||
assertTickJudgement(1, HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.SliderTailHit);
|
||||
assertSliderJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
base.LoadComplete();
|
||||
|
||||
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
|
||||
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
|
||||
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => updateConnectingPath());
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
updateConnectingPath();
|
||||
}
|
||||
|
||||
@@ -4,20 +4,15 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -41,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<DragEvent> DragInProgress;
|
||||
public Action DragEnded;
|
||||
|
||||
public List<PathControlPoint> PointsInSegment;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
@@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> hitObjectVersion;
|
||||
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
|
||||
cachePoints(hitObject);
|
||||
|
||||
hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the hit object path type for batch operations.
|
||||
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(hitObject);
|
||||
updatePathType();
|
||||
}));
|
||||
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
@@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
|
||||
|
||||
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
private void updatePathType()
|
||||
{
|
||||
if (ControlPoint.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (PointsInSegment.Count > 3)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
|
||||
if (PointsInSegment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
|
||||
+54
-3
@@ -14,10 +14,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -76,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
public void EnsureValidPathTypes()
|
||||
{
|
||||
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
|
||||
|
||||
foreach (var controlPoint in controlPoints)
|
||||
{
|
||||
if (controlPoint.Type != null)
|
||||
{
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
pointsInCurrentSegment.Clear();
|
||||
}
|
||||
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
}
|
||||
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
}
|
||||
|
||||
private void ensureValidPathType(IReadOnlyList<PathControlPoint> segment)
|
||||
{
|
||||
if (segment.Count == 0)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (segment.Count > 3)
|
||||
first.Type = PathType.BEZIER;
|
||||
|
||||
if (segment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
first.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
|
||||
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
|
||||
@@ -240,7 +286,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
{
|
||||
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
{
|
||||
@@ -249,8 +296,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
@@ -339,6 +386,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
|
||||
EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
public void DragEnded() => changeHandler?.EndChange();
|
||||
@@ -412,6 +461,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
});
|
||||
|
||||
if (countOfState == totalCount)
|
||||
|
||||
@@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
segmentStart.Type = PathType.BEZIER;
|
||||
break;
|
||||
}
|
||||
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
private void updateCursor()
|
||||
|
||||
@@ -254,6 +254,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// Move the control points from the insertion index onwards to make room for the insertion
|
||||
controlPoints.Insert(insertionIndex, pathControlPoint);
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
return pathControlPoint;
|
||||
@@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
controlPoints.Remove(c);
|
||||
}
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
// Snap the slider to the current beat divisor before checking length validity.
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons())
|
||||
.Concat(new[]
|
||||
{
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
|
||||
});
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModDepth : ModWithVisibilityAdjustment, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override string Name => "Depth";
|
||||
public override string Acronym => "DP";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cube;
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "3D. Almost.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame), typeof(ModWithVisibilityAdjustment) }).ToArray();
|
||||
|
||||
private static readonly Vector3 camera_position = new Vector3(OsuPlayfield.BASE_SIZE.X * 0.5f, OsuPlayfield.BASE_SIZE.Y * 0.5f, -200);
|
||||
private readonly float sliderMinDepth = depthForScale(1.5f); // Depth at which slider's scale will be 1.5f
|
||||
|
||||
[SettingSource("Maximum depth", "How far away objects appear.", 0)]
|
||||
public BindableFloat MaxDepth { get; } = new BindableFloat(100)
|
||||
{
|
||||
Precision = 10,
|
||||
MinValue = 50,
|
||||
MaxValue = 200
|
||||
};
|
||||
|
||||
[SettingSource("Show Approach Circles", "Whether approach circles should be visible.", 1)]
|
||||
public BindableBool ShowApproachCircles { get; } = new BindableBool(true);
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state);
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// Hide judgment displays and follow points as they won't make any sense.
|
||||
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
|
||||
drawableRuleset.Playfield.DisplayJudgements.Value = false;
|
||||
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
|
||||
}
|
||||
|
||||
private void applyTransform(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
if (!ShowApproachCircles.Value)
|
||||
{
|
||||
var hitObject = (OsuHitObject)drawable.HitObject;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
|
||||
using (circle.BeginAbsoluteSequence(appearTime))
|
||||
circle.ApproachCircle.Hide();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
double time = playfield.Time.Current;
|
||||
|
||||
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
|
||||
{
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
processHitObject(time, circle);
|
||||
break;
|
||||
|
||||
case DrawableSlider slider:
|
||||
processSlider(time, slider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processHitObject(double time, DrawableOsuHitObject drawable)
|
||||
{
|
||||
var hitObject = drawable.HitObject;
|
||||
|
||||
// Circles are always moving at the constant speed. They'll fade out before reaching the camera even at extreme conditions (AR 11, max depth).
|
||||
double speed = MaxDepth.Value / hitObject.TimePreempt;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
float z = MaxDepth.Value - (float)((Math.Max(time, appearTime) - appearTime) * speed);
|
||||
|
||||
float scale = scaleForDepth(z);
|
||||
drawable.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
|
||||
drawable.Scale = new Vector2(scale);
|
||||
}
|
||||
|
||||
private void processSlider(double time, DrawableSlider drawableSlider)
|
||||
{
|
||||
var hitObject = drawableSlider.HitObject;
|
||||
|
||||
double baseSpeed = MaxDepth.Value / hitObject.TimePreempt;
|
||||
double appearTime = hitObject.StartTime - hitObject.TimePreempt;
|
||||
|
||||
// Allow slider to move at a constant speed if its scale at the end time will be lower than 1.5f
|
||||
float zEnd = MaxDepth.Value - (float)((Math.Max(hitObject.StartTime + hitObject.Duration, appearTime) - appearTime) * baseSpeed);
|
||||
|
||||
if (zEnd > sliderMinDepth)
|
||||
{
|
||||
processHitObject(time, drawableSlider);
|
||||
return;
|
||||
}
|
||||
|
||||
double offsetAfterStartTime = hitObject.Duration + 500;
|
||||
double slowSpeed = Math.Min(-sliderMinDepth / offsetAfterStartTime, baseSpeed);
|
||||
|
||||
double decelerationTime = hitObject.TimePreempt * 0.2;
|
||||
float decelerationDistance = (float)(decelerationTime * (baseSpeed + slowSpeed) * 0.5);
|
||||
|
||||
float z;
|
||||
|
||||
if (time < hitObject.StartTime - decelerationTime)
|
||||
{
|
||||
float fullDistance = decelerationDistance + (float)(baseSpeed * (hitObject.TimePreempt - decelerationTime));
|
||||
z = fullDistance - (float)((Math.Max(time, appearTime) - appearTime) * baseSpeed);
|
||||
}
|
||||
else if (time < hitObject.StartTime)
|
||||
{
|
||||
double timeOffset = time - (hitObject.StartTime - decelerationTime);
|
||||
double deceleration = (slowSpeed - baseSpeed) / decelerationTime;
|
||||
z = decelerationDistance - (float)(baseSpeed * timeOffset + deceleration * timeOffset * timeOffset * 0.5);
|
||||
}
|
||||
else
|
||||
{
|
||||
double endTime = hitObject.StartTime + offsetAfterStartTime;
|
||||
z = -(float)((Math.Min(time, endTime) - hitObject.StartTime) * slowSpeed);
|
||||
}
|
||||
|
||||
float scale = scaleForDepth(z);
|
||||
drawableSlider.Position = toPlayfieldPosition(scale, hitObject.StackedPosition);
|
||||
drawableSlider.Scale = new Vector2(scale);
|
||||
}
|
||||
|
||||
private static float scaleForDepth(float depth) => -camera_position.Z / Math.Max(1f, depth - camera_position.Z);
|
||||
|
||||
private static float depthForScale(float scale) => -camera_position.Z / scale + camera_position.Z;
|
||||
|
||||
private static Vector2 toPlayfieldPosition(float scale, Vector2 positionAtZeroDepth)
|
||||
{
|
||||
return (positionAtZeroDepth - camera_position.Xy) * scale + camera_position.Xy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
if (drawable is DrawableSlider s)
|
||||
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
|
||||
s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s);
|
||||
}
|
||||
|
||||
private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
|
||||
@@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||
public void OnSliderTrackingChange(DrawableSlider e)
|
||||
{
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
FlashlightDim = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||
|
||||
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray();
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
|
||||
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) };
|
||||
|
||||
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
|
||||
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles), typeof(OsuModDepth) };
|
||||
|
||||
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
|
||||
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -66,6 +67,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
private Container<DrawableSliderRepeat> repeatContainer;
|
||||
private PausableSkinnableSound slidingSample;
|
||||
|
||||
private readonly LayoutValue drawSizeLayout;
|
||||
|
||||
public DrawableSlider()
|
||||
: this(null)
|
||||
{
|
||||
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
};
|
||||
AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -240,27 +244,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
else if (slidingSample.IsPlaying)
|
||||
slidingSample.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// During slider path editing, the PlaySliderBody is scheduled to refresh once on Update.
|
||||
// It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity
|
||||
// to update its Size and PathOffset beforehand, ensuring correct placement.
|
||||
|
||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
if (hitObject is ITrackSnaking s)
|
||||
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
}
|
||||
foreach (DrawableSliderRepeat repeat in repeatContainer)
|
||||
repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
|
||||
Size = SliderBody?.Size ?? Vector2.Zero;
|
||||
OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
|
||||
|
||||
if (DrawSize != Vector2.Zero)
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize);
|
||||
Vector2 pos = Vector2.Divide(OriginPosition, DrawSize);
|
||||
foreach (var obj in NestedHitObjects)
|
||||
obj.RelativeAnchorPosition = childAnchorPosition;
|
||||
Ball.RelativeAnchorPosition = childAnchorPosition;
|
||||
obj.RelativeAnchorPosition = pos;
|
||||
Ball.RelativeAnchorPosition = pos;
|
||||
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject
|
||||
{
|
||||
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
||||
|
||||
|
||||
@@ -47,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.
|
||||
@@ -114,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,
|
||||
}
|
||||
|
||||
@@ -1,15 +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 osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which tracks the current end snaking position of a slider.
|
||||
/// </summary>
|
||||
public interface ITrackSnaking
|
||||
{
|
||||
void UpdateSnakingPosition(Vector2 start, Vector2 end);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
repeatCount = value;
|
||||
updateNestedPositions();
|
||||
endPositionCache.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
|
||||
@@ -14,16 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public abstract class SliderEndCircle : HitCircle
|
||||
{
|
||||
private readonly Slider slider;
|
||||
protected readonly Slider Slider;
|
||||
|
||||
protected SliderEndCircle(Slider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
Slider = slider;
|
||||
}
|
||||
|
||||
public int RepeatIndex { get; set; }
|
||||
|
||||
public double SpanDuration => slider.SpanDuration;
|
||||
public double SpanDuration => Slider.SpanDuration;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
else
|
||||
{
|
||||
// The first end circle should fade in with the slider.
|
||||
TimePreempt += StartTime - slider.StartTime;
|
||||
TimePreempt += StartTime - Slider.StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
// When classic slider mechanics are enabled, this result comes from the tail.
|
||||
return 0.02;
|
||||
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.LargeTickHit:
|
||||
switch (result.HitObject)
|
||||
{
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonCursor : OsuCursorSprite
|
||||
public partial class ArgonCursor : SkinnableCursor
|
||||
{
|
||||
public ArgonCursor()
|
||||
{
|
||||
|
||||
@@ -62,7 +62,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
/// </remarks>
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
if (Result.IsMiss())
|
||||
if (Result == HitResult.IgnoreMiss || Result == HitResult.LargeTickMiss)
|
||||
{
|
||||
this.RotateTo(-45);
|
||||
this.ScaleTo(1.8f);
|
||||
this.ScaleTo(1.2f, 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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
|
||||
|
||||
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => Refresh());
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(Refresh));
|
||||
|
||||
AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
|
||||
AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
TriangleScale = 1.2f;
|
||||
HideAlphaDiscrepancies = false;
|
||||
ClampToDrawable = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -9,8 +9,11 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCursor : OsuCursorSprite
|
||||
public partial class LegacyCursor : SkinnableCursor
|
||||
{
|
||||
private const float pressed_scale = 1.3f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
private readonly ISkin skin;
|
||||
private bool spin;
|
||||
|
||||
@@ -51,5 +54,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
if (spin)
|
||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
||||
}
|
||||
|
||||
public override void Expand()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale)
|
||||
.ScaleTo(pressed_scale, 100, Easing.Out);
|
||||
}
|
||||
|
||||
public override void Contract()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale, 100, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation;
|
||||
|
||||
float turnRatio = spinningMiddle.Texture != null ? 0.5f : 1;
|
||||
discTop.Rotation = DrawableSpinner.RotationTracker.Rotation * turnRatio;
|
||||
spinningMiddle.Rotation = DrawableSpinner.RotationTracker.Rotation;
|
||||
|
||||
discBottom.Rotation = discTop.Rotation / 3;
|
||||
|
||||
glow.Alpha = DrawableSpinner.Progress;
|
||||
|
||||
@@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private void load(OsuRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
|
||||
|
||||
AddInternal(ripplePool);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
|
||||
@@ -24,15 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public const float SIZE = 28;
|
||||
|
||||
private const float pressed_scale = 1.2f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
private bool cursorExpand;
|
||||
|
||||
private SkinnableDrawable cursorSprite;
|
||||
private Container cursorScaleContainer = null!;
|
||||
|
||||
private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite;
|
||||
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
@@ -57,23 +54,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = cursorScaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
InternalChild = CreateCursorContent();
|
||||
|
||||
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 +68,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
calculateCursorScale();
|
||||
cursorScale.Value = CalculateCursorScale();
|
||||
}
|
||||
|
||||
private void calculateCursorScale()
|
||||
protected virtual Drawable CreateCursorContent() => cursorScaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
|
||||
protected virtual float CalculateCursorScale()
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
|
||||
@@ -94,7 +93,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)
|
||||
@@ -106,10 +105,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
if (!cursorExpand) return;
|
||||
|
||||
expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf);
|
||||
skinnableCursor.Expand();
|
||||
}
|
||||
|
||||
public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad);
|
||||
public void Contract() => skinnableCursor.Contract();
|
||||
|
||||
/// <summary>
|
||||
/// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
|
||||
@@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
public static float GetScaleForCircleSize(float circleSize) =>
|
||||
1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
|
||||
|
||||
private partial class DefaultCursor : OsuCursorSprite
|
||||
private partial class DefaultCursor : SkinnableCursor
|
||||
{
|
||||
public DefaultCursor()
|
||||
{
|
||||
|
||||
@@ -1,19 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public abstract partial class OsuCursorSprite : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The an optional piece of the cursor to expand when in a clicked state.
|
||||
/// If null, the whole cursor will be affected by expansion.
|
||||
/// </summary>
|
||||
public Drawable ExpandTarget { get; protected set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public abstract partial class SkinnableCursor : CompositeDrawable
|
||||
{
|
||||
private const float pressed_scale = 1.2f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
public virtual void Expand()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale)
|
||||
.ScaleTo(pressed_scale, 400, Easing.OutElasticHalf);
|
||||
}
|
||||
|
||||
public virtual void Contract()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale, 400, Easing.OutQuad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The an optional piece of the cursor to expand when in a clicked state.
|
||||
/// If null, the whole cursor will be affected by expansion.
|
||||
/// </summary>
|
||||
public Drawable? ExpandTarget { get; protected set; }
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,11 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -35,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
private readonly ProxyContainer spinnerProxies;
|
||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||
|
||||
private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler;
|
||||
|
||||
public SmokeContainer Smoke { get; }
|
||||
public FollowPointRenderer FollowPoints { get; }
|
||||
|
||||
@@ -42,8 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
|
||||
|
||||
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
|
||||
|
||||
private readonly Container judgementAboveHitObjectLayer;
|
||||
|
||||
public OsuPlayfield()
|
||||
@@ -65,24 +63,15 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
HitPolicy = new StartTimeOrderedHitPolicy();
|
||||
|
||||
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);
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableOsuJudgement>(new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
HitResult.Miss,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.IgnoreMiss,
|
||||
}, onJudgementLoaded));
|
||||
|
||||
NewResult += onNewResult;
|
||||
}
|
||||
@@ -182,10 +171,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
if (!poolDictionary.TryGetValue(result.Type, out var pool))
|
||||
return;
|
||||
var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject));
|
||||
|
||||
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
|
||||
if (explosion == null)
|
||||
return;
|
||||
|
||||
judgementLayer.Add(explosion);
|
||||
|
||||
@@ -201,31 +190,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public void Add(Drawable proxy) => AddInternal(proxy);
|
||||
}
|
||||
|
||||
private partial class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
|
||||
{
|
||||
private readonly HitResult result;
|
||||
private readonly Action<DrawableOsuJudgement> onLoaded;
|
||||
|
||||
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
|
||||
: base(20)
|
||||
{
|
||||
this.result = result;
|
||||
this.onLoaded = onLoaded;
|
||||
}
|
||||
|
||||
protected override DrawableOsuJudgement CreateNewDrawable()
|
||||
{
|
||||
var judgement = base.CreateNewDrawable();
|
||||
|
||||
// just a placeholder to initialise the correct drawable hierarchy for this pool.
|
||||
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
|
||||
|
||||
onLoaded?.Invoke(judgement);
|
||||
|
||||
return judgement;
|
||||
}
|
||||
}
|
||||
|
||||
private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry
|
||||
{
|
||||
public OsuHitObjectLifetimeEntry(HitObject hitObject)
|
||||
|
||||
@@ -65,12 +65,25 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
public Action ResumeRequested;
|
||||
private Container scaleTransitionContainer;
|
||||
|
||||
public OsuClickToResumeCursor()
|
||||
{
|
||||
RelativePositionAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override Container CreateCursorContent() => scaleTransitionContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = base.CreateCursorContent(),
|
||||
};
|
||||
|
||||
protected override float CalculateCursorScale() =>
|
||||
// Force minimum cursor size so it's easily clickable
|
||||
Math.Max(1f, base.CalculateCursorScale());
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateColour();
|
||||
@@ -92,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!IsHovered)
|
||||
return false;
|
||||
|
||||
this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||
|
||||
ResumeRequested?.Invoke();
|
||||
return true;
|
||||
@@ -108,7 +121,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public void Appear() => Schedule(() =>
|
||||
{
|
||||
updateColour();
|
||||
this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
|
||||
|
||||
// importantly, we perform the scale transition on an underlying container rather than the whole cursor
|
||||
// to prevent attempts of abuse by the scale change in the cursor's hitbox (see: https://github.com/ppy/osu/issues/26477).
|
||||
scaleTransitionContainer.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
|
||||
});
|
||||
|
||||
private void updateColour()
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TaikoHealthProcessorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestHitsOnlyGreat()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsAboveThreshold()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.GreaterThan(0.5));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsBelowThreshold()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.LessThan(0.5));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrumRollOnly()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSwellOnly()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -65,11 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
@@ -31,11 +31,39 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
/// </summary>
|
||||
private double hpMissMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// Sum of all achievable health increases throughout the map.
|
||||
/// Used to determine if there are any objects that give health.
|
||||
/// If there are none, health will be forcibly pulled up to 1 to avoid cases of impassable maps.
|
||||
/// </summary>
|
||||
private double sumOfMaxHealthIncreases;
|
||||
|
||||
public TaikoHealthProcessor()
|
||||
: base(0.5)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ApplyResultInternal(JudgementResult result)
|
||||
{
|
||||
base.ApplyResultInternal(result);
|
||||
sumOfMaxHealthIncreases += result.Judgement.MaxHealthIncrease;
|
||||
}
|
||||
|
||||
protected override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
base.RevertResultInternal(result);
|
||||
sumOfMaxHealthIncreases -= result.Judgement.MaxHealthIncrease;
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
if (storeResults && sumOfMaxHealthIncreases == 0)
|
||||
Health.Value = 1;
|
||||
sumOfMaxHealthIncreases = 0;
|
||||
}
|
||||
|
||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
base.ApplyBeatmap(beatmap);
|
||||
|
||||
@@ -69,9 +69,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim),
|
||||
new KeyBinding(InputKey.D, TaikoAction.LeftRim),
|
||||
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.F, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.J, TaikoAction.RightCentre),
|
||||
new KeyBinding(InputKey.K, TaikoAction.RightRim),
|
||||
|
||||
@@ -1,8 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -10,7 +8,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -42,29 +39,29 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public Container UnderlayElements { get; private set; } = null!;
|
||||
|
||||
private Container<HitExplosion> hitExplosionContainer;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
private ScrollingHitObjectContainer drumRollHitContainer;
|
||||
internal Drawable HitTarget;
|
||||
private SkinnableDrawable mascot;
|
||||
private Container<HitExplosion> hitExplosionContainer = null!;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer = null!;
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer = null!;
|
||||
private ScrollingHitObjectContainer drumRollHitContainer = null!;
|
||||
internal Drawable HitTarget = null!;
|
||||
private SkinnableDrawable mascot = null!;
|
||||
|
||||
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
|
||||
private JudgementPooler<DrawableTaikoJudgement> judgementPooler = null!;
|
||||
private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
|
||||
|
||||
private ProxyContainer topLevelHitContainer;
|
||||
private InputDrum inputDrum;
|
||||
private Container rightArea;
|
||||
private ProxyContainer topLevelHitContainer = null!;
|
||||
private InputDrum inputDrum = null!;
|
||||
private Container rightArea = null!;
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="Playfield.AddNested"/> is purposefully not called on this to prevent i.e. being able to interact
|
||||
/// with bar lines in the editor.
|
||||
/// </remarks>
|
||||
private BarLinePlayfield barLinePlayfield;
|
||||
private BarLinePlayfield barLinePlayfield = null!;
|
||||
|
||||
private Container barLineContent;
|
||||
private Container hitObjectContent;
|
||||
private Container overlayContent;
|
||||
private Container barLineContent = null!;
|
||||
private Container hitObjectContent = null!;
|
||||
private Container overlayContent = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
@@ -202,13 +199,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
|
||||
{
|
||||
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
|
||||
explosionPools.Add(result, new HitExplosionPool(result));
|
||||
}
|
||||
HitResult[] usableHitResults = Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray();
|
||||
|
||||
AddRangeInternal(judgementPools.Values);
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableTaikoJudgement>(usableHitResults));
|
||||
|
||||
foreach (var result in usableHitResults)
|
||||
explosionPools.Add(result, new HitExplosionPool(result));
|
||||
AddRangeInternal(explosionPools.Values);
|
||||
}
|
||||
|
||||
@@ -339,7 +335,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
if (!result.Type.IsScorable())
|
||||
break;
|
||||
|
||||
judgementContainer.Add(judgementPools[result.Type].Get(j => j.Apply(result, judgedObject)));
|
||||
var judgement = judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject));
|
||||
|
||||
if (judgement == null)
|
||||
return;
|
||||
|
||||
judgementContainer.Add(judgement);
|
||||
|
||||
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
|
||||
addExplosion(judgedObject, result.Type, type);
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
@@ -247,6 +251,123 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfStableScorePreserved()
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
// local partial implementation of legacy score encoder
|
||||
// this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION`
|
||||
// and we want to emulate a stable score here
|
||||
using (var sw = new SerializationWriter(memoryStream, true))
|
||||
{
|
||||
sw.Write((byte)0); // ruleset id (osu!)
|
||||
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||
sw.Write("username"); // irrelevant to this test
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||
sw.Write((ushort)198); // count300
|
||||
sw.Write((ushort)1); // count100
|
||||
sw.Write((ushort)0); // count50
|
||||
sw.Write((ushort)0); // countGeki
|
||||
sw.Write((ushort)0); // countKatu
|
||||
sw.Write((ushort)1); // countMiss
|
||||
sw.Write(12345678); // total score, irrelevant to this test
|
||||
sw.Write((ushort)1000); // max combo, irrelevant to this test
|
||||
sw.Write(false); // full combo, irrelevant to this test
|
||||
sw.Write((int)LegacyMods.Hidden); // mods
|
||||
sw.Write(string.Empty); // hp graph, irrelevant
|
||||
sw.Write(DateTime.Now); // date, irrelevant
|
||||
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
|
||||
sw.Write((long)1234); // legacy online ID, irrelevant
|
||||
}
|
||||
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300)));
|
||||
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfLazerScorePreserved()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
scoreInfo.Mods = new Mod[] { new OsuModFlashlight() };
|
||||
scoreInfo.Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 199,
|
||||
[HitResult.Miss] = 1,
|
||||
[HitResult.LargeTickHit] = 1,
|
||||
};
|
||||
scoreInfo.MaximumStatistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 200,
|
||||
[HitResult.LargeTickHit] = 1,
|
||||
};
|
||||
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = scoreInfo,
|
||||
};
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestEffortFallbackToLegacy()
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
// local partial implementation of legacy score encoder
|
||||
// this is done half for readability, half because we want to emulate an old lazer score here
|
||||
// that does not have everything that `LegacyScoreEncoder` now writes to the replay
|
||||
using (var sw = new SerializationWriter(memoryStream, true))
|
||||
{
|
||||
sw.Write((byte)0); // ruleset id (osu!)
|
||||
sw.Write(LegacyScoreEncoder.FIRST_LAZER_VERSION); // version
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||
sw.Write("username"); // irrelevant to this test
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||
sw.Write((ushort)198); // count300
|
||||
sw.Write((ushort)0); // count100
|
||||
sw.Write((ushort)1); // count50
|
||||
sw.Write((ushort)0); // countGeki
|
||||
sw.Write((ushort)0); // countKatu
|
||||
sw.Write((ushort)1); // countMiss
|
||||
sw.Write(12345678); // total score, irrelevant to this test
|
||||
sw.Write((ushort)1000); // max combo, irrelevant to this test
|
||||
sw.Write(false); // full combo, irrelevant to this test
|
||||
sw.Write((int)LegacyMods.Hidden); // mods
|
||||
sw.Write(string.Empty); // hp graph, irrelevant
|
||||
sw.Write(DateTime.Now); // date, irrelevant
|
||||
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
|
||||
sw.Write((long)1234); // legacy online ID, irrelevant
|
||||
// importantly, no compressed `LegacyReplaySoloScoreInfo` here
|
||||
}
|
||||
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 50) / (200 * 300)));
|
||||
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
||||
});
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Tests.Chat
|
||||
});
|
||||
|
||||
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
|
||||
AddUntilStep("message postesd", () => !channel.Messages.Any(m => m is LocalMessage));
|
||||
AddUntilStep("message posted", () => !channel.Messages.Any(m => m is LocalMessage));
|
||||
|
||||
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
|
||||
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
|
||||
@@ -146,6 +146,23 @@ namespace osu.Game.Tests.Chat
|
||||
AddAssert("channel has no more messages", () => channel.Messages, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommandNameCaseInsensitivity()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
|
||||
AddStep("post /me command", () => channelManager.PostCommand("ME DANCES"));
|
||||
AddUntilStep("/me command received", () => channel.Messages.Last().Content.Contains("DANCES"));
|
||||
AddStep("post /help command", () => channelManager.PostCommand("HeLp"));
|
||||
AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands"));
|
||||
}
|
||||
|
||||
private void handlePostMessageRequest(PostMessageRequest request)
|
||||
{
|
||||
var message = new Message(++currentMessageId)
|
||||
|
||||
@@ -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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -182,9 +183,63 @@ namespace osu.Game.Tests.Database
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
|
||||
{
|
||||
RulesetInfo rulesetInfo = null!;
|
||||
ScoreInfo scoreInfo = null!;
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
|
||||
AddStep("Add unavailable ruleset", () => Realm.Write(r => r.Add(rulesetInfo = new RulesetInfo
|
||||
{
|
||||
ShortName = Guid.NewGuid().ToString(),
|
||||
Available = available
|
||||
})));
|
||||
|
||||
AddStep("Add score for unavailable ruleset", () => Realm.Write(r => r.Add(scoreInfo = new ScoreInfo(
|
||||
ruleset: rulesetInfo,
|
||||
beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000001
|
||||
})));
|
||||
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonLegacyScoreNotSubjectToUpgrades()
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
|
||||
AddStep("Add score which requires upgrade (and has beatmap)", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000005,
|
||||
LegacyTotalScore = 123456,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005));
|
||||
}
|
||||
|
||||
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
||||
{
|
||||
protected override int TimeToSleepDuringGameplay => 10;
|
||||
|
||||
public bool Completed => ProcessingTask.IsCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
@@ -310,6 +312,36 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModBelongsToRuleset()
|
||||
{
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), Array.Empty<Mod>()));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime() }));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new OsuModAccuracyChallenge() }));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new ModAccuracyChallenge() }), Is.False);
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new TaikoModFlashlight() }), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFormatScoreMultiplier()
|
||||
{
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9999).ToString(), "0.99x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0).ToString(), "1.00x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.0001).ToString(), "1.01x");
|
||||
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.899999999999999).ToString(), "0.90x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.9).ToString(), "0.90x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(0.900000000000001).ToString(), "0.90x");
|
||||
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.099999999999999).ToString(), "1.10x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.1).ToString(), "1.10x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.100000000000001).ToString(), "1.10x");
|
||||
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.045).ToString(), "1.05x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.05).ToString(), "1.05x");
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
|
||||
}
|
||||
|
||||
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
@@ -339,6 +371,16 @@ namespace osu.Game.Tests.Mods
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
}
|
||||
|
||||
public class EditableMod : Mod
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => Multiplier;
|
||||
|
||||
public double Multiplier = 1;
|
||||
}
|
||||
|
||||
public interface IModCompatibilitySpecification
|
||||
{
|
||||
}
|
||||
|
||||
@@ -127,8 +127,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
||||
Assert.IsNull(texture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowHighResolutionSprites()
|
||||
{
|
||||
var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x");
|
||||
var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = false };
|
||||
|
||||
var texture = legacySkin.GetTexture("hitcircle");
|
||||
|
||||
Assert.IsNotNull(texture);
|
||||
Assert.That(texture.ScaleAdjust, Is.EqualTo(1));
|
||||
|
||||
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
|
||||
|
||||
Assert.IsNotNull(twoTimesTexture);
|
||||
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(1));
|
||||
|
||||
Assert.AreNotEqual(texture, twoTimesTexture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllowHighResolutionSprites()
|
||||
{
|
||||
var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x");
|
||||
var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = true };
|
||||
|
||||
var texture = legacySkin.GetTexture("hitcircle");
|
||||
|
||||
Assert.IsNotNull(texture);
|
||||
Assert.That(texture.ScaleAdjust, Is.EqualTo(2));
|
||||
|
||||
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
|
||||
|
||||
Assert.IsNotNull(twoTimesTexture);
|
||||
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(2));
|
||||
|
||||
Assert.AreEqual(texture, twoTimesTexture);
|
||||
}
|
||||
|
||||
private class TestLegacySkin : LegacySkin
|
||||
{
|
||||
public bool HighResolutionSprites { get; set; } = true;
|
||||
|
||||
protected override bool AllowHighResolutionSprites => HighResolutionSprites;
|
||||
|
||||
public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
|
||||
: base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,,3,3,3
|
||||
@@ -18,10 +18,12 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@@ -84,6 +86,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SliderTailHit, HitResult.SliderTailHit, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||
@@ -96,6 +99,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SliderTailHit, HitResult.SliderTailHit, 32_696)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)]
|
||||
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
||||
@@ -167,6 +171,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.Perfect, HitResult.Miss)]
|
||||
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
|
||||
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
|
||||
[TestCase(HitResult.SliderTailHit, HitResult.LargeTickMiss)]
|
||||
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
|
||||
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
|
||||
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
|
||||
@@ -187,6 +192,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.SmallTickHit, false)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SliderTailHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, false)]
|
||||
[TestCase(HitResult.LargeBonus, false)]
|
||||
public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
|
||||
@@ -207,6 +213,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SliderTailHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, false)]
|
||||
[TestCase(HitResult.LargeBonus, false)]
|
||||
public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
|
||||
@@ -227,6 +234,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.SmallTickHit, false)]
|
||||
[TestCase(HitResult.LargeTickMiss, false)]
|
||||
[TestCase(HitResult.LargeTickHit, false)]
|
||||
[TestCase(HitResult.SliderTailHit, false)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
|
||||
@@ -247,6 +255,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, false)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SliderTailHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
|
||||
@@ -267,6 +276,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SliderTailHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
|
||||
@@ -377,6 +387,42 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalGrades()
|
||||
{
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.S));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSilverGrades()
|
||||
{
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
|
||||
|
||||
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSilverGradesModsAppliedFirst()
|
||||
{
|
||||
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
@@ -114,5 +114,25 @@ namespace osu.Game.Tests.Skins
|
||||
Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseArrayWithSomeEmptyElements()
|
||||
{
|
||||
var decoder = new LegacyManiaSkinDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("mania-skin-broken-array.ini"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var configs = decoder.Decode(stream);
|
||||
|
||||
Assert.That(configs.Count, Is.EqualTo(1));
|
||||
Assert.That(configs[0].ColumnLineWidth.Length, Is.EqualTo(5));
|
||||
Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero
|
||||
Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[3], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[4], Is.EqualTo(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
|
||||
AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s));
|
||||
AddToggleStep("Masking", m => triangles.Masking = m);
|
||||
AddToggleStep("ClampToDrawable", c => triangles.ClampToDrawable = c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White);
|
||||
AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red));
|
||||
AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red));
|
||||
AddToggleStep("Masking", m => maskedTriangles.Masking = m);
|
||||
AddToggleStep("ClampToDrawable", c => maskedTriangles.ClampToDrawable = c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
if (sameRuleset)
|
||||
{
|
||||
AddUntilStep("prompt for save dialog shown", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
|
||||
AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog).PerformOkAction());
|
||||
AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog)?.PerformOkAction());
|
||||
}
|
||||
|
||||
// ensure editor loader didn't resume.
|
||||
|
||||
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
@@ -94,8 +95,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestAddAudioTrack()
|
||||
{
|
||||
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
|
||||
AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType<Timeline>().FirstOrDefault()?.IsLoaded == true);
|
||||
|
||||
AddStep("enter setup mode", () => InputManager.Key(Key.F4));
|
||||
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||
AddAssert("switch track to real track", () =>
|
||||
{
|
||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -17,6 +19,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@@ -69,6 +72,48 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectedRetainedOverUndo()
|
||||
{
|
||||
AddStep("Select first timing point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170);
|
||||
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170);
|
||||
|
||||
AddStep("Adjust offset", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for offset changed", () =>
|
||||
{
|
||||
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
|
||||
});
|
||||
|
||||
AddStep("simulate undo", () =>
|
||||
{
|
||||
var clone = editorBeatmap.ControlPointInfo.DeepClone();
|
||||
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
|
||||
foreach (var group in clone.Groups)
|
||||
{
|
||||
foreach (var cp in group.ControlPoints)
|
||||
editorBeatmap.ControlPointInfo.Add(group.Time, cp);
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("selection retained", () =>
|
||||
{
|
||||
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackingCurrentTimeWhileRunning()
|
||||
{
|
||||
@@ -134,6 +179,43 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType<OsuScrollContainer>().First().IsScrolledToEnd());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditThenClickAwayAppliesChanges()
|
||||
{
|
||||
AddStep("Add two control points", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(1000, new TimingControlPoint());
|
||||
editorBeatmap.ControlPointInfo.Add(2000, new TimingControlPoint());
|
||||
});
|
||||
|
||||
AddStep("Select second timing point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("Scroll to end", () => timingScreen.ChildrenOfType<ControlPointSettings>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
AddStep("Modify time signature", () =>
|
||||
{
|
||||
var timeSignatureTextBox = Child.ChildrenOfType<LabelledTimeSignature.TimeSignatureBox>().Single().ChildrenOfType<TextBox>().Single();
|
||||
InputManager.MoveMouseTo(timeSignatureTextBox);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
|
||||
Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1", StringComparison.Ordinal));
|
||||
timeSignatureTextBox.Current.Value = "1";
|
||||
});
|
||||
|
||||
AddStep("Select first timing point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Second timing point changed time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.Last().TimeSignature.Numerator == 1);
|
||||
AddAssert("First timing point preserved time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.First().TimeSignature.Numerator == 4);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
@@ -159,5 +160,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Type = HitResult.Perfect
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimulateDrain()
|
||||
{
|
||||
ScheduledDelegate del = null!;
|
||||
|
||||
AddStep("simulate drain", () => del = Scheduler.AddDelayed(() => healthProcessor.Health.Value -= 0.00025f * Time.Elapsed, 0, true));
|
||||
AddUntilStep("wait until zero", () => healthProcessor.Health.Value == 0);
|
||||
AddStep("cancel drain", () => del.Cancel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@@ -44,7 +45,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2)
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreFromDifferentBeatmap()
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
|
||||
BeatmapInfo = TestResources.CreateTestBeatmapSetInfo().Beatmaps.First(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -59,7 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
|
||||
Mods = new Mod[] { new OsuModRelax() }
|
||||
Mods = new Mod[] { new OsuModRelax() },
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -77,7 +95,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -105,7 +124,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private readonly Bindable<bool> showHealth = new Bindable<bool>();
|
||||
|
||||
private HealthProcessor healthProcessor;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
@@ -29,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("create layer", () =>
|
||||
{
|
||||
Child = new HealthProcessorContainer(healthProcessor)
|
||||
Child = new HealthProcessorContainer(this.healthProcessor = healthProcessor)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = layer = new FailingLayer()
|
||||
@@ -50,12 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
|
||||
{
|
||||
if (layer != null)
|
||||
layer.Current.Value = val;
|
||||
healthProcessor.Health.Value = val;
|
||||
});
|
||||
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer fade is visible", () => layer.ChildrenOfType<Container>().First().Alpha > 0.1f);
|
||||
AddStep("set health to 1", () => layer.Current.Value = 1f);
|
||||
AddStep("set health to 1", () => healthProcessor.Health.Value = 1f);
|
||||
AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType<Container>().First().IsPresent);
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
create(new DrainingHealthProcessor(0));
|
||||
AddUntilStep("layer is visible", () => layer.IsPresent);
|
||||
AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
create(new AccumulatingHealthProcessor(1));
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestLayerVisibilityWithDrainingProcessor()
|
||||
{
|
||||
create(new DrainingHealthProcessor(0));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddWaitStep("wait for potential fade", 10);
|
||||
AddAssert("layer is still visible", () => layer.IsPresent);
|
||||
}
|
||||
@@ -92,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
create(new DrainingHealthProcessor(0));
|
||||
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
|
||||
AddStep("don't show health", () => showHealth.Value = false);
|
||||
AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
|
||||
@@ -147,6 +147,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoDuplicates()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddAssert("Check no duplicates",
|
||||
() => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Count(),
|
||||
() => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Select(c => c.ResultName.Text).Distinct().Count()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCycleDisplayModes()
|
||||
{
|
||||
@@ -163,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private int hiddenCount()
|
||||
{
|
||||
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit);
|
||||
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit));
|
||||
return num.Result.ResultCount.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@@ -487,13 +486,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
}
|
||||
|
||||
private class TestMod : Mod, IApplicableToScoreProcessor
|
||||
private class TestMod : OsuModDoubleTime, IApplicableToScoreProcessor
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
|
||||
public bool Applied { get; private set; }
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
|
||||
@@ -15,11 +15,13 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -34,12 +36,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private Func<RulesetInfo, IBeatmap> createCustomBeatmap;
|
||||
private Func<Ruleset> createCustomRuleset;
|
||||
private Func<Mod[]> createCustomMods;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false);
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset)
|
||||
{
|
||||
if (createCustomMods != null)
|
||||
SelectedMods.Value = SelectedMods.Value.Concat(createCustomMods()).ToList();
|
||||
|
||||
return new FakeImportingPlayer(false);
|
||||
}
|
||||
|
||||
protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player;
|
||||
|
||||
@@ -277,13 +286,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null)
|
||||
[Test]
|
||||
public void TestNoSubmissionWithModsOfDifferentRuleset()
|
||||
{
|
||||
prepareTestAPI(true);
|
||||
|
||||
createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
AddAssert("gameplay not loaded", () => Player.DrawableRuleset == null);
|
||||
|
||||
AddStep("exit", () => Player.Exit());
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null, Func<Mod[]> createMods = null)
|
||||
{
|
||||
CreateTest(() => AddStep("set up requirements", () =>
|
||||
{
|
||||
this.allowFail = allowFail;
|
||||
createCustomBeatmap = createBeatmap;
|
||||
createCustomRuleset = createRuleset;
|
||||
createCustomMods = createMods;
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f };
|
||||
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 600, UseRelativeSize = { Value = false } };
|
||||
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
|
||||
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };
|
||||
|
||||
@@ -35,6 +35,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
healthProcessor.Health.Value -= 0.0001f * Time.Elapsed;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHealthDisplayIncrementing()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneIntroMusicActionHandling : OsuGameTestScene
|
||||
{
|
||||
private GlobalActionContainer globalActionContainer => Game.ChildrenOfType<GlobalActionContainer>().First();
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
CreateNewGame();
|
||||
// we do not want to progress to main menu immediately, hence the override and lack of `ConfirmAtMainMenu()` call here.
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseDuringIntro()
|
||||
{
|
||||
AddUntilStep("Wait for music", () => Game?.MusicController.IsPlaying == true);
|
||||
|
||||
// Check that pause doesn't work during intro sequence.
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddAssert("Still playing before menu", () => Game?.MusicController.IsPlaying == true);
|
||||
AddUntilStep("Wait for main menu", () => Game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
|
||||
|
||||
// Check that toggling after intro still works.
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddUntilStep("Music paused", () => Game?.MusicController.IsPlaying == false && Game?.MusicController.UserPauseRequested == true);
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddUntilStep("Music resumed", () => Game?.MusicController.IsPlaying == true && Game?.MusicController.UserPauseRequested == false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneMainMenu : OsuGameTestScene
|
||||
{
|
||||
private SystemTitle systemTitle => Game.ChildrenOfType<SystemTitle>().Single();
|
||||
|
||||
[Test]
|
||||
public void TestSystemTitle()
|
||||
{
|
||||
AddStep("set system title", () => systemTitle.Current.Value = new APISystemTitle
|
||||
{
|
||||
Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png",
|
||||
Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023",
|
||||
});
|
||||
AddAssert("system title not visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddStep("enter menu", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("system title visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddStep("set another title", () => systemTitle.Current.Value = new APISystemTitle
|
||||
{
|
||||
Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png",
|
||||
Url = @"https://osu.ppy.sh/community/contests/189",
|
||||
});
|
||||
AddStep("set title with nonexistent image", () => systemTitle.Current.Value = new APISystemTitle
|
||||
{
|
||||
Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2
|
||||
Url = @"https://osu.ppy.sh/community/contests/189",
|
||||
});
|
||||
AddStep("unset system title", () => systemTitle.Current.Value = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-6
@@ -1,19 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneDisclaimer : ScreenTestScene
|
||||
public partial class TestSceneSupporterDisplay : OsuTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("load disclaimer", () => LoadScreen(new Disclaimer()));
|
||||
AddStep("create display", () =>
|
||||
{
|
||||
Child = new SupporterDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("toggle support", () =>
|
||||
{
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Mods
|
||||
public void TestMaximumAchievableAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
Mod = new OsuModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 }
|
||||
},
|
||||
@@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Mods
|
||||
public void TestStandardAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
Mod = new OsuModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 },
|
||||
AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard }
|
||||
|
||||
@@ -19,8 +19,10 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -302,6 +304,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectableMouseHandling()
|
||||
{
|
||||
bool resultsRequested = false;
|
||||
|
||||
AddStep("reset flag", () => resultsRequested = false);
|
||||
createPlaylist(p =>
|
||||
{
|
||||
p.AllowSelection = true;
|
||||
p.AllowShowingResults = true;
|
||||
p.RequestResults = _ => resultsRequested = true;
|
||||
});
|
||||
|
||||
AddStep("move mouse to first item title", () =>
|
||||
{
|
||||
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
|
||||
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
|
||||
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
|
||||
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
|
||||
// implies being clickable.
|
||||
AddUntilStep("first item title hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.True);
|
||||
|
||||
AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<GrayButton>().ElementAt(5)));
|
||||
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("results requested", () => resultsRequested);
|
||||
}
|
||||
|
||||
private void moveToItem(int index, Vector2? offset = null)
|
||||
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DrawableRoomPlaylistItem>().ElementAt(index), offset));
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene
|
||||
{
|
||||
private FreeModSelectOverlay freeModSelectOverlay;
|
||||
private FooterButtonFreeMods footerButtonFreeMods;
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectAllViaFooterButtonThenDeselectFromOverlay()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
|
||||
|
||||
AddStep("click footer select all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(footerButtonFreeMods);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "all"));
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
|
||||
}
|
||||
|
||||
private void createFreeModSelect()
|
||||
{
|
||||
AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
|
||||
AddStep("create free mod select screen", () => Children = new Drawable[]
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
freeModSelectOverlay = new FreeModSelectOverlay
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
},
|
||||
footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay)
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
|
||||
},
|
||||
});
|
||||
AddUntilStep("all column content loaded",
|
||||
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
@@ -134,10 +172,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
var allAvailableMods = availableMods.Value
|
||||
.Where(pair => pair.Key != ModType.System)
|
||||
.SelectMany(pair => pair.Value)
|
||||
.SelectMany(pair => ModUtils.FlattenMods(pair.Value))
|
||||
.Where(mod => mod.UserPlayable && mod.HasImplementation)
|
||||
.ToList();
|
||||
|
||||
if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count)
|
||||
return false;
|
||||
|
||||
foreach (var availableMod in allAvailableMods)
|
||||
{
|
||||
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
|
||||
|
||||
@@ -29,6 +29,9 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
@@ -690,6 +693,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
|
||||
|
||||
AddAssert("check is fail", () =>
|
||||
{
|
||||
var scoreInfo = ((ResultsScreen)multiplayerComponents.CurrentScreen).Score;
|
||||
|
||||
return !scoreInfo.Passed && scoreInfo.Rank == ScoreRank.F;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -1001,6 +1011,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayStartsWhileInSongSelectWithDifferentRuleset()
|
||||
{
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = { Value = "Test Room" },
|
||||
QueueMode = { Value = QueueMode.AllPlayers },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
|
||||
},
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new TaikoRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } }));
|
||||
AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready));
|
||||
AddStep("press edit on second item", () => this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(i => i.Item.RulesetID == 1)
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single().TriggerClick());
|
||||
|
||||
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
|
||||
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1);
|
||||
|
||||
AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad);
|
||||
AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing);
|
||||
AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden)));
|
||||
}
|
||||
|
||||
private void enterGameplay()
|
||||
{
|
||||
pressReadyButton();
|
||||
|
||||
@@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
@@ -799,11 +800,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("attempt exit", () =>
|
||||
{
|
||||
for (int i = 0; i < 2; ++i)
|
||||
Game.ScreenStack.CurrentScreen.Exit();
|
||||
});
|
||||
AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2);
|
||||
AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
|
||||
}
|
||||
|
||||
@@ -838,6 +835,24 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog is ConfirmExitDialog);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickSkinEditorDoesntNukeSkin()
|
||||
{
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddStep("open", () => InputManager.Key(Key.Space));
|
||||
AddStep("skin", () => InputManager.Key(Key.E));
|
||||
AddStep("editor", () => InputManager.Key(Key.S));
|
||||
AddStep("and close immediately", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddStep("open again", () => InputManager.Key(Key.S));
|
||||
|
||||
Player player = null;
|
||||
|
||||
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||
AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType<ArgonHealthDisplay>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchScreenDetectionAtSongSelect()
|
||||
{
|
||||
@@ -942,6 +957,35 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitSongSelectAndImmediatelyClickLogo()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("press escape and then click logo immediately", () =>
|
||||
{
|
||||
InputManager.Key(Key.Escape);
|
||||
clickLogoWhenNotCurrent();
|
||||
});
|
||||
|
||||
void clickLogoWhenNotCurrent()
|
||||
{
|
||||
if (songSelect.IsCurrentScreen())
|
||||
Scheduler.AddOnce(clickLogoWhenNotCurrent);
|
||||
else
|
||||
{
|
||||
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Func<Player> playToResults()
|
||||
{
|
||||
var player = playToCompletion();
|
||||
|
||||
@@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
var cardContainer = this.ChildrenOfType<ReverseChildIDFillFlowContainer<BeatmapCard>>().Single().Parent;
|
||||
var expandedContent = this.ChildrenOfType<ExpandedContentScrollContainer>().Single();
|
||||
return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer.ScreenSpaceDrawQuad.Contains(v));
|
||||
return expandedContent.ScreenSpaceDrawQuad.GetVertices().ToArray().All(v => cardContainer!.ScreenSpaceDrawQuad.Contains(v));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("update not received", () => update == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
|
||||
{
|
||||
int userId = getUserId();
|
||||
long scoreId = getScoreId();
|
||||
setUpUser(userId);
|
||||
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
|
||||
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
|
||||
AddUntilStep("update received", () => update != null);
|
||||
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000));
|
||||
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
|
||||
}
|
||||
|
||||
private int nextUserId = 2000;
|
||||
private long nextScoreId = 50000;
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
@@ -29,6 +31,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private UserGridPanel boundPanel1;
|
||||
private TestUserListPanel boundPanel2;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Resolved]
|
||||
private IRulesetStore rulesetStore { get; set; }
|
||||
|
||||
@@ -85,8 +90,25 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsOnline = false,
|
||||
LastVisit = DateTimeOffset.Now
|
||||
})
|
||||
},
|
||||
}),
|
||||
new UserRankPanel(new APIUser
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
|
||||
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
|
||||
}) { Width = 300 },
|
||||
new UserRankPanel(new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
|
||||
}) { Width = 300 }
|
||||
}
|
||||
};
|
||||
|
||||
boundPanel1.Status.BindTo(status);
|
||||
@@ -136,6 +158,23 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserStatisticsChange()
|
||||
{
|
||||
AddStep("update statistics", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics
|
||||
{
|
||||
GlobalRank = RNG.Next(100000),
|
||||
CountryRank = RNG.Next(100000)
|
||||
});
|
||||
});
|
||||
AddStep("set statistics to empty", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics());
|
||||
});
|
||||
}
|
||||
|
||||
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
|
||||
|
||||
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
|
||||
@@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Cached]
|
||||
private readonly Bindable<APIWikiPage> wikiPageData = new Bindable<APIWikiPage>(new APIWikiPage
|
||||
{
|
||||
Title = "Main Page",
|
||||
Path = "Main_Page",
|
||||
Title = "Main page",
|
||||
Path = WikiOverlay.INDEX_PATH,
|
||||
});
|
||||
|
||||
private TestHeader header;
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
};
|
||||
}
|
||||
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_Page
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_page
|
||||
private const string main_page_markdown =
|
||||
"---\nlayout: main_page\n---\n\n<!-- Do not add any empty lines inside this div. -->\n\n<div class=\"wiki-main-page__blurb\">\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n</div>\n\n<div class=\"wiki-main-page__panels\">\n<div class=\"wiki-main-page-panel wiki-main-page-panel--full\">\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n</div>\n</div>\n";
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user