mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'master' into add-minimise-on-focus-loss-setting
This commit is contained in:
commit
093001438c
2
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@ -42,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
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1227.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.114.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
739
osu.Desktop/NVAPI.cs
Normal file
739
osu.Desktop/NVAPI.cs
Normal file
@ -0,0 +1,739 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable IDE1006 // Naming rule violation
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Logging;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal static class NVAPI
|
||||
{
|
||||
private const string osu_filename = "osu!.exe";
|
||||
|
||||
// This is a good reference:
|
||||
// https://github.com/errollw/Warp-and-Blend-Quadros/blob/master/WarpBlend-Quadros/UnwarpAll-Quadros/include/nvapi.h
|
||||
// Note our Stride == their VERSION (e.g. NVDRS_SETTING_VER)
|
||||
|
||||
public const int MAX_PHYSICAL_GPUS = 64;
|
||||
public const int UNICODE_STRING_MAX = 2048;
|
||||
|
||||
public const string APPLICATION_NAME = @"osu!";
|
||||
public const string PROFILE_NAME = @"osu!";
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus EnumPhysicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
|
||||
|
||||
public static readonly EnumPhysicalGPUsDelegate EnumPhysicalGPUs;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus EnumLogicalGPUsDelegate([Out] IntPtr[] gpuHandles, out int gpuCount);
|
||||
|
||||
public static readonly EnumLogicalGPUsDelegate EnumLogicalGPUs;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetSystemTypeDelegate(IntPtr gpuHandle, out NvSystemType systemType);
|
||||
|
||||
public static readonly GetSystemTypeDelegate GetSystemType;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetGPUTypeDelegate(IntPtr gpuHandle, out NvGpuType gpuType);
|
||||
|
||||
public static readonly GetGPUTypeDelegate GetGPUType;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus CreateSessionDelegate(out IntPtr sessionHandle);
|
||||
|
||||
public static CreateSessionDelegate CreateSession;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus LoadSettingsDelegate(IntPtr sessionHandle);
|
||||
|
||||
public static LoadSettingsDelegate LoadSettings;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus FindApplicationByNameDelegate(IntPtr sessionHandle, [MarshalAs(UnmanagedType.BStr)] string appName, out IntPtr profileHandle, ref NvApplication application);
|
||||
|
||||
public static FindApplicationByNameDelegate FindApplicationByName;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetCurrentGlobalProfileDelegate(IntPtr sessionHandle, out IntPtr profileHandle);
|
||||
|
||||
public static GetCurrentGlobalProfileDelegate GetCurrentGlobalProfile;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetProfileInfoDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvProfile profile);
|
||||
|
||||
public static GetProfileInfoDelegate GetProfileInfo;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
public delegate NvStatus GetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, NvSettingID settingID, ref NvSetting setting);
|
||||
|
||||
public static GetSettingDelegate GetSetting;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus CreateProfileDelegate(IntPtr sessionHandle, ref NvProfile profile, out IntPtr profileHandle);
|
||||
|
||||
private static readonly CreateProfileDelegate CreateProfile;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus SetSettingDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvSetting setting);
|
||||
|
||||
private static readonly SetSettingDelegate SetSetting;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus EnumApplicationsDelegate(IntPtr sessionHandle, IntPtr profileHandle, uint startIndex, ref uint appCount, [In, Out, MarshalAs(UnmanagedType.LPArray)] NvApplication[] applications);
|
||||
|
||||
private static readonly EnumApplicationsDelegate EnumApplications;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus CreateApplicationDelegate(IntPtr sessionHandle, IntPtr profileHandle, ref NvApplication application);
|
||||
|
||||
private static readonly CreateApplicationDelegate CreateApplication;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate NvStatus SaveSettingsDelegate(IntPtr sessionHandle);
|
||||
|
||||
private static readonly SaveSettingsDelegate SaveSettings;
|
||||
|
||||
public static NvStatus Status { get; private set; } = NvStatus.OK;
|
||||
public static bool Available { get; private set; }
|
||||
|
||||
private static IntPtr sessionHandle;
|
||||
|
||||
public static bool IsUsingOptimusDedicatedGpu
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return false;
|
||||
|
||||
if (!IsLaptop)
|
||||
return false;
|
||||
|
||||
IntPtr profileHandle;
|
||||
if (!getProfile(out profileHandle, out _, out bool _))
|
||||
return false;
|
||||
|
||||
// Get the optimus setting
|
||||
NvSetting setting;
|
||||
if (!getSetting(NvSettingID.SHIM_RENDERING_MODE_ID, profileHandle, out setting))
|
||||
return false;
|
||||
|
||||
return (setting.U32CurrentValue & (uint)NvShimSetting.SHIM_RENDERING_MODE_ENABLE) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsLaptop
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return false;
|
||||
|
||||
// Make sure that this is a laptop.
|
||||
var gpus = new IntPtr[64];
|
||||
if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < gpuCount; i++)
|
||||
{
|
||||
if (checkError(GetSystemType(gpus[i], out var type)))
|
||||
return false;
|
||||
|
||||
if (type == NvSystemType.LAPTOP)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static NvThreadControlSetting ThreadedOptimisations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Available)
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
IntPtr profileHandle;
|
||||
if (!getProfile(out profileHandle, out _, out bool _))
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Get the threaded optimisations setting
|
||||
NvSetting setting;
|
||||
if (!getSetting(NvSettingID.OGL_THREAD_CONTROL_ID, profileHandle, out setting))
|
||||
return NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
return (NvThreadControlSetting)setting.U32CurrentValue;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!Available)
|
||||
return;
|
||||
|
||||
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
|
||||
|
||||
Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the profile contains the current application.
|
||||
/// </summary>
|
||||
/// <returns>If the profile contains the current application.</returns>
|
||||
private static bool containsApplication(IntPtr profileHandle, NvProfile profile, out NvApplication application)
|
||||
{
|
||||
application = new NvApplication
|
||||
{
|
||||
Version = NvApplication.Stride
|
||||
};
|
||||
|
||||
if (profile.NumOfApps == 0)
|
||||
return false;
|
||||
|
||||
NvApplication[] applications = new NvApplication[profile.NumOfApps];
|
||||
applications[0].Version = NvApplication.Stride;
|
||||
|
||||
uint numApps = profile.NumOfApps;
|
||||
|
||||
if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
|
||||
return false;
|
||||
|
||||
for (uint i = 0; i < numApps; i++)
|
||||
{
|
||||
if (applications[i].AppName == osu_filename)
|
||||
{
|
||||
application = applications[i];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the profile of the current application.
|
||||
/// </summary>
|
||||
/// <param name="profileHandle">The profile handle.</param>
|
||||
/// <param name="application">The current application description.</param>
|
||||
/// <param name="isApplicationSpecific">If this profile is not a global (default) profile.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool getProfile(out IntPtr profileHandle, out NvApplication application, out bool isApplicationSpecific)
|
||||
{
|
||||
application = new NvApplication
|
||||
{
|
||||
Version = NvApplication.Stride
|
||||
};
|
||||
|
||||
isApplicationSpecific = true;
|
||||
|
||||
if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
|
||||
{
|
||||
isApplicationSpecific = false;
|
||||
if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile.
|
||||
/// </summary>
|
||||
/// <param name="profileHandle">The profile handle.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool createProfile(out IntPtr profileHandle)
|
||||
{
|
||||
NvProfile newProfile = new NvProfile
|
||||
{
|
||||
Version = NvProfile.Stride,
|
||||
IsPredefined = 0,
|
||||
ProfileName = PROFILE_NAME,
|
||||
GPUSupport = new uint[32]
|
||||
};
|
||||
|
||||
newProfile.GPUSupport[0] = 1;
|
||||
|
||||
if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a setting from the profile.
|
||||
/// </summary>
|
||||
/// <param name="settingId">The setting to retrieve.</param>
|
||||
/// <param name="profileHandle">The profile handle to retrieve the setting from.</param>
|
||||
/// <param name="setting">The setting.</param>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool getSetting(NvSettingID settingId, IntPtr profileHandle, out NvSetting setting)
|
||||
{
|
||||
setting = new NvSetting
|
||||
{
|
||||
Version = NvSetting.Stride,
|
||||
SettingID = settingId
|
||||
};
|
||||
|
||||
if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool setSetting(NvSettingID settingId, uint settingValue)
|
||||
{
|
||||
NvApplication application;
|
||||
IntPtr profileHandle;
|
||||
bool isApplicationSpecific;
|
||||
if (!getProfile(out profileHandle, out application, out isApplicationSpecific))
|
||||
return false;
|
||||
|
||||
if (!isApplicationSpecific)
|
||||
{
|
||||
// We don't want to interfere with the user's other settings, so let's create a separate config for osu!
|
||||
if (!createProfile(out profileHandle))
|
||||
return false;
|
||||
}
|
||||
|
||||
NvSetting newSetting = new NvSetting
|
||||
{
|
||||
Version = NvSetting.Stride,
|
||||
SettingID = settingId,
|
||||
U32CurrentValue = settingValue
|
||||
};
|
||||
|
||||
// Set the thread state
|
||||
if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
|
||||
return false;
|
||||
|
||||
// Get the profile (needed to check app count)
|
||||
NvProfile profile = new NvProfile
|
||||
{
|
||||
Version = NvProfile.Stride
|
||||
};
|
||||
if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
|
||||
return false;
|
||||
|
||||
if (!containsApplication(profileHandle, profile, out application))
|
||||
{
|
||||
// Need to add the current application to the profile
|
||||
application.IsPredefined = 0;
|
||||
|
||||
application.AppName = osu_filename;
|
||||
application.UserFriendlyName = APPLICATION_NAME;
|
||||
|
||||
if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save!
|
||||
return !checkError(SaveSettings(sessionHandle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a session to access the driver configuration.
|
||||
/// </summary>
|
||||
/// <returns>If the operation succeeded.</returns>
|
||||
private static bool createSession()
|
||||
{
|
||||
if (checkError(CreateSession(out sessionHandle)))
|
||||
return false;
|
||||
|
||||
// Load settings into session
|
||||
if (checkError(LoadSettings(sessionHandle)))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool checkError(NvStatus status)
|
||||
{
|
||||
Status = status;
|
||||
return status != NvStatus.OK;
|
||||
}
|
||||
|
||||
static NVAPI()
|
||||
{
|
||||
// TODO: check whether gpu vendor contains NVIDIA before attempting load?
|
||||
|
||||
try
|
||||
{
|
||||
// Try to load NVAPI
|
||||
if ((IntPtr.Size == 4 && loadLibrary(@"nvapi.dll") == IntPtr.Zero)
|
||||
|| (IntPtr.Size == 8 && loadLibrary(@"nvapi64.dll") == IntPtr.Zero))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeDelegate initialize;
|
||||
getDelegate(0x0150E828, out initialize);
|
||||
|
||||
if (initialize?.Invoke() == NvStatus.OK)
|
||||
{
|
||||
// IDs can be found here: https://github.com/jNizM/AHK_NVIDIA_NvAPI/blob/master/info/NvAPI_IDs.txt
|
||||
|
||||
getDelegate(0xE5AC921F, out EnumPhysicalGPUs);
|
||||
getDelegate(0x48B3EA59, out EnumLogicalGPUs);
|
||||
getDelegate(0xBAAABFCC, out GetSystemType);
|
||||
getDelegate(0xC33BAEB1, out GetGPUType);
|
||||
getDelegate(0x0694D52E, out CreateSession);
|
||||
getDelegate(0x375DBD6B, out LoadSettings);
|
||||
getDelegate(0xEEE566B2, out FindApplicationByName);
|
||||
getDelegate(0x617BFF9F, out GetCurrentGlobalProfile);
|
||||
getDelegate(0x577DD202, out SetSetting);
|
||||
getDelegate(0x61CD6FD6, out GetProfileInfo);
|
||||
getDelegate(0x73BF8338, out GetSetting);
|
||||
getDelegate(0xCC176068, out CreateProfile);
|
||||
getDelegate(0x7FA2173A, out EnumApplications);
|
||||
getDelegate(0x4347A9DE, out CreateApplication);
|
||||
getDelegate(0xFCBC7E14, out SaveSettings);
|
||||
}
|
||||
|
||||
if (createSession())
|
||||
Available = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void getDelegate<T>(uint id, out T newDelegate) where T : class
|
||||
{
|
||||
IntPtr ptr = IntPtr.Size == 4 ? queryInterface32(id) : queryInterface64(id);
|
||||
newDelegate = ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
|
||||
private static extern IntPtr loadLibrary(string dllToLoad);
|
||||
|
||||
[DllImport(@"nvapi.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr queryInterface32(uint id);
|
||||
|
||||
[DllImport(@"nvapi64.dll", EntryPoint = "nvapi_QueryInterface", CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr queryInterface64(uint id);
|
||||
|
||||
private delegate NvStatus InitializeDelegate();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct NvSetting
|
||||
{
|
||||
public uint Version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string SettingName;
|
||||
|
||||
public NvSettingID SettingID;
|
||||
public uint SettingType;
|
||||
public uint SettingLocation;
|
||||
public uint IsCurrentPredefined;
|
||||
public uint IsPredefinedValid;
|
||||
|
||||
public uint U32PredefinedValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string StringPredefinedValue;
|
||||
|
||||
public uint U32CurrentValue;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string StringCurrentValue;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetting)) | (1 << 16);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
|
||||
internal struct NvProfile
|
||||
{
|
||||
public uint Version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string ProfileName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValArray)]
|
||||
public uint[] GPUSupport;
|
||||
|
||||
public uint IsPredefined;
|
||||
public uint NumOfApps;
|
||||
public uint NumOfSettings;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvProfile)) | (1 << 16);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8, CharSet = CharSet.Unicode)]
|
||||
internal struct NvApplication
|
||||
{
|
||||
public uint Version;
|
||||
public uint IsPredefined;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string AppName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string UserFriendlyName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string Launcher;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
|
||||
public string FileInFolder;
|
||||
|
||||
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
|
||||
}
|
||||
|
||||
internal enum NvStatus
|
||||
{
|
||||
OK = 0, // Success. Request is completed.
|
||||
ERROR = -1, // Generic error
|
||||
LIBRARY_NOT_FOUND = -2, // NVAPI support library cannot be loaded.
|
||||
NO_IMPLEMENTATION = -3, // not implemented in current driver installation
|
||||
API_NOT_INITIALIZED = -4, // Initialize has not been called (successfully)
|
||||
INVALID_ARGUMENT = -5, // The argument/parameter value is not valid or NULL.
|
||||
NVIDIA_DEVICE_NOT_FOUND = -6, // No NVIDIA display driver, or NVIDIA GPU driving a display, was found.
|
||||
END_ENUMERATION = -7, // No more items to enumerate
|
||||
INVALID_HANDLE = -8, // Invalid handle
|
||||
INCOMPATIBLE_STRUCT_VERSION = -9, // An argument's structure version is not supported
|
||||
HANDLE_INVALIDATED = -10, // The handle is no longer valid (likely due to GPU or display re-configuration)
|
||||
OPENGL_CONTEXT_NOT_CURRENT = -11, // No NVIDIA OpenGL context is current (but needs to be)
|
||||
INVALID_POINTER = -14, // An invalid pointer, usually NULL, was passed as a parameter
|
||||
NO_GL_EXPERT = -12, // OpenGL Expert is not supported by the current drivers
|
||||
INSTRUMENTATION_DISABLED = -13, // OpenGL Expert is supported, but driver instrumentation is currently disabled
|
||||
NO_GL_NSIGHT = -15, // OpenGL does not support Nsight
|
||||
|
||||
EXPECTED_LOGICAL_GPU_HANDLE = -100, // Expected a logical GPU handle for one or more parameters
|
||||
EXPECTED_PHYSICAL_GPU_HANDLE = -101, // Expected a physical GPU handle for one or more parameters
|
||||
EXPECTED_DISPLAY_HANDLE = -102, // Expected an NV display handle for one or more parameters
|
||||
INVALID_COMBINATION = -103, // The combination of parameters is not valid.
|
||||
NOT_SUPPORTED = -104, // Requested feature is not supported in the selected GPU
|
||||
PORTID_NOT_FOUND = -105, // No port ID was found for the I2C transaction
|
||||
EXPECTED_UNATTACHED_DISPLAY_HANDLE = -106, // Expected an unattached display handle as one of the input parameters.
|
||||
INVALID_PERF_LEVEL = -107, // Invalid perf level
|
||||
DEVICE_BUSY = -108, // Device is busy; request not fulfilled
|
||||
NV_PERSIST_FILE_NOT_FOUND = -109, // NV persist file is not found
|
||||
PERSIST_DATA_NOT_FOUND = -110, // NV persist data is not found
|
||||
EXPECTED_TV_DISPLAY = -111, // Expected a TV output display
|
||||
EXPECTED_TV_DISPLAY_ON_DCONNECTOR = -112, // Expected a TV output on the D Connector - HDTV_EIAJ4120.
|
||||
NO_ACTIVE_SLI_TOPOLOGY = -113, // SLI is not active on this device.
|
||||
SLI_RENDERING_MODE_NOTALLOWED = -114, // Setup of SLI rendering mode is not possible right now.
|
||||
EXPECTED_DIGITAL_FLAT_PANEL = -115, // Expected a digital flat panel.
|
||||
ARGUMENT_EXCEED_MAX_SIZE = -116, // Argument exceeds the expected size.
|
||||
DEVICE_SWITCHING_NOT_ALLOWED = -117, // Inhibit is ON due to one of the flags in NV_GPU_DISPLAY_CHANGE_INHIBIT or SLI active.
|
||||
TESTING_CLOCKS_NOT_SUPPORTED = -118, // Testing of clocks is not supported.
|
||||
UNKNOWN_UNDERSCAN_CONFIG = -119, // The specified underscan config is from an unknown source (e.g. INF)
|
||||
TIMEOUT_RECONFIGURING_GPU_TOPO = -120, // Timeout while reconfiguring GPUs
|
||||
DATA_NOT_FOUND = -121, // Requested data was not found
|
||||
EXPECTED_ANALOG_DISPLAY = -122, // Expected an analog display
|
||||
NO_VIDLINK = -123, // No SLI video bridge is present
|
||||
REQUIRES_REBOOT = -124, // NVAPI requires a reboot for the settings to take effect
|
||||
INVALID_HYBRID_MODE = -125, // The function is not supported with the current Hybrid mode.
|
||||
MIXED_TARGET_TYPES = -126, // The target types are not all the same
|
||||
SYSWOW64_NOT_SUPPORTED = -127, // The function is not supported from 32-bit on a 64-bit system.
|
||||
IMPLICIT_SET_GPU_TOPOLOGY_CHANGE_NOT_ALLOWED = -128, // There is no implicit GPU topology active. Use SetHybridMode to change topology.
|
||||
REQUEST_USER_TO_CLOSE_NON_MIGRATABLE_APPS = -129, // Prompt the user to close all non-migratable applications.
|
||||
OUT_OF_MEMORY = -130, // Could not allocate sufficient memory to complete the call.
|
||||
WAS_STILL_DRAWING = -131, // The previous operation that is transferring information to or from this surface is incomplete.
|
||||
FILE_NOT_FOUND = -132, // The file was not found.
|
||||
TOO_MANY_UNIQUE_STATE_OBJECTS = -133, // There are too many unique instances of a particular type of state object.
|
||||
INVALID_CALL = -134, // The method call is invalid. For example, a method's parameter may not be a valid pointer.
|
||||
D3D10_1_LIBRARY_NOT_FOUND = -135, // d3d10_1.dll cannot be loaded.
|
||||
FUNCTION_NOT_FOUND = -136, // Couldn't find the function in the loaded DLL.
|
||||
INVALID_USER_PRIVILEGE = -137, // Current User is not Admin.
|
||||
EXPECTED_NON_PRIMARY_DISPLAY_HANDLE = -138, // The handle corresponds to GDIPrimary.
|
||||
EXPECTED_COMPUTE_GPU_HANDLE = -139, // Setting Physx GPU requires that the GPU is compute-capable.
|
||||
STEREO_NOT_INITIALIZED = -140, // The Stereo part of NVAPI failed to initialize completely. Check if the stereo driver is installed.
|
||||
STEREO_REGISTRY_ACCESS_FAILED = -141, // Access to stereo-related registry keys or values has failed.
|
||||
STEREO_REGISTRY_PROFILE_TYPE_NOT_SUPPORTED = -142, // The given registry profile type is not supported.
|
||||
STEREO_REGISTRY_VALUE_NOT_SUPPORTED = -143, // The given registry value is not supported.
|
||||
STEREO_NOT_ENABLED = -144, // Stereo is not enabled and the function needed it to execute completely.
|
||||
STEREO_NOT_TURNED_ON = -145, // Stereo is not turned on and the function needed it to execute completely.
|
||||
STEREO_INVALID_DEVICE_INTERFACE = -146, // Invalid device interface.
|
||||
STEREO_PARAMETER_OUT_OF_RANGE = -147, // Separation percentage or JPEG image capture quality is out of [0-100] range.
|
||||
STEREO_FRUSTUM_ADJUST_MODE_NOT_SUPPORTED = -148, // The given frustum adjust mode is not supported.
|
||||
TOPO_NOT_POSSIBLE = -149, // The mosaic topology is not possible given the current state of the hardware.
|
||||
MODE_CHANGE_FAILED = -150, // An attempt to do a display resolution mode change has failed.
|
||||
D3D11_LIBRARY_NOT_FOUND = -151, // d3d11.dll/d3d11_beta.dll cannot be loaded.
|
||||
INVALID_ADDRESS = -152, // Address is outside of valid range.
|
||||
STRING_TOO_SMALL = -153, // The pre-allocated string is too small to hold the result.
|
||||
MATCHING_DEVICE_NOT_FOUND = -154, // The input does not match any of the available devices.
|
||||
DRIVER_RUNNING = -155, // Driver is running.
|
||||
DRIVER_NOTRUNNING = -156, // Driver is not running.
|
||||
ERROR_DRIVER_RELOAD_REQUIRED = -157, // A driver reload is required to apply these settings.
|
||||
SET_NOT_ALLOWED = -158, // Intended setting is not allowed.
|
||||
ADVANCED_DISPLAY_TOPOLOGY_REQUIRED = -159, // Information can't be returned due to "advanced display topology".
|
||||
SETTING_NOT_FOUND = -160, // Setting is not found.
|
||||
SETTING_SIZE_TOO_LARGE = -161, // Setting size is too large.
|
||||
TOO_MANY_SETTINGS_IN_PROFILE = -162, // There are too many settings for a profile.
|
||||
PROFILE_NOT_FOUND = -163, // Profile is not found.
|
||||
PROFILE_NAME_IN_USE = -164, // Profile name is duplicated.
|
||||
PROFILE_NAME_EMPTY = -165, // Profile name is empty.
|
||||
EXECUTABLE_NOT_FOUND = -166, // Application not found in the Profile.
|
||||
EXECUTABLE_ALREADY_IN_USE = -167, // Application already exists in the other profile.
|
||||
DATATYPE_MISMATCH = -168, // Data Type mismatch
|
||||
PROFILE_REMOVED = -169, // The profile passed as parameter has been removed and is no longer valid.
|
||||
UNREGISTERED_RESOURCE = -170, // An unregistered resource was passed as a parameter.
|
||||
ID_OUT_OF_RANGE = -171, // The DisplayId corresponds to a display which is not within the normal outputId range.
|
||||
DISPLAYCONFIG_VALIDATION_FAILED = -172, // Display topology is not valid so the driver cannot do a mode set on this configuration.
|
||||
DPMST_CHANGED = -173, // Display Port Multi-Stream topology has been changed.
|
||||
INSUFFICIENT_BUFFER = -174, // Input buffer is insufficient to hold the contents.
|
||||
ACCESS_DENIED = -175, // No access to the caller.
|
||||
MOSAIC_NOT_ACTIVE = -176, // The requested action cannot be performed without Mosaic being enabled.
|
||||
SHARE_RESOURCE_RELOCATED = -177, // The surface is relocated away from video memory.
|
||||
REQUEST_USER_TO_DISABLE_DWM = -178, // The user should disable DWM before calling NvAPI.
|
||||
D3D_DEVICE_LOST = -179, // D3D device status is D3DERR_DEVICELOST or D3DERR_DEVICENOTRESET - the user has to reset the device.
|
||||
INVALID_CONFIGURATION = -180, // The requested action cannot be performed in the current state.
|
||||
STEREO_HANDSHAKE_NOT_DONE = -181, // Call failed as stereo handshake not completed.
|
||||
EXECUTABLE_PATH_IS_AMBIGUOUS = -182, // The path provided was too short to determine the correct NVDRS_APPLICATION
|
||||
DEFAULT_STEREO_PROFILE_IS_NOT_DEFINED = -183, // Default stereo profile is not currently defined
|
||||
DEFAULT_STEREO_PROFILE_DOES_NOT_EXIST = -184, // Default stereo profile does not exist
|
||||
CLUSTER_ALREADY_EXISTS = -185, // A cluster is already defined with the given configuration.
|
||||
DPMST_DISPLAY_ID_EXPECTED = -186, // The input display id is not that of a multi stream enabled connector or a display device in a multi stream topology
|
||||
INVALID_DISPLAY_ID = -187, // The input display id is not valid or the monitor associated to it does not support the current operation
|
||||
STREAM_IS_OUT_OF_SYNC = -188, // While playing secure audio stream, stream goes out of sync
|
||||
INCOMPATIBLE_AUDIO_DRIVER = -189, // Older audio driver version than required
|
||||
VALUE_ALREADY_SET = -190, // Value already set, setting again not allowed.
|
||||
TIMEOUT = -191, // Requested operation timed out
|
||||
GPU_WORKSTATION_FEATURE_INCOMPLETE = -192, // The requested workstation feature set has incomplete driver internal allocation resources
|
||||
STEREO_INIT_ACTIVATION_NOT_DONE = -193, // Call failed because InitActivation was not called.
|
||||
SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
|
||||
SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled.
|
||||
INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer.
|
||||
ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value.
|
||||
ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed.
|
||||
FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date.
|
||||
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
|
||||
}
|
||||
|
||||
internal enum NvSystemType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
LAPTOP = 1,
|
||||
DESKTOP = 2
|
||||
}
|
||||
|
||||
internal enum NvGpuType
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
IGPU = 1, // Integrated
|
||||
DGPU = 2, // Discrete
|
||||
}
|
||||
|
||||
internal enum NvSettingID : uint
|
||||
{
|
||||
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
|
||||
OGL_DEEP_COLOR_SCANOUT_ID = 0x2097C2F6,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_ID = 0x206A6582,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_FRACTIONAL_ID = 0x206C4581,
|
||||
OGL_DEFAULT_SWAP_INTERVAL_SIGN_ID = 0x20655CFA,
|
||||
OGL_EVENT_LOG_SEVERITY_THRESHOLD_ID = 0x209DF23E,
|
||||
OGL_EXTENSION_STRING_VERSION_ID = 0x20FF7493,
|
||||
OGL_FORCE_BLIT_ID = 0x201F619F,
|
||||
OGL_FORCE_STEREO_ID = 0x204D9A0C,
|
||||
OGL_IMPLICIT_GPU_AFFINITY_ID = 0x20D0F3E6,
|
||||
OGL_MAX_FRAMES_ALLOWED_ID = 0x208E55E3,
|
||||
OGL_MULTIMON_ID = 0x200AEBFC,
|
||||
OGL_OVERLAY_PIXEL_TYPE_ID = 0x209AE66F,
|
||||
OGL_OVERLAY_SUPPORT_ID = 0x206C28C4,
|
||||
OGL_QUALITY_ENHANCEMENTS_ID = 0x20797D6C,
|
||||
OGL_SINGLE_BACKDEPTH_BUFFER_ID = 0x20A29055,
|
||||
OGL_THREAD_CONTROL_ID = 0x20C1221E,
|
||||
OGL_TRIPLE_BUFFER_ID = 0x20FDD1F9,
|
||||
OGL_VIDEO_EDITING_MODE_ID = 0x20EE02B4,
|
||||
AA_BEHAVIOR_FLAGS_ID = 0x10ECDB82,
|
||||
AA_MODE_ALPHATOCOVERAGE_ID = 0x10FC2D9C,
|
||||
AA_MODE_GAMMACORRECTION_ID = 0x107D639D,
|
||||
AA_MODE_METHOD_ID = 0x10D773D2,
|
||||
AA_MODE_REPLAY_ID = 0x10D48A85,
|
||||
AA_MODE_SELECTOR_ID = 0x107EFC5B,
|
||||
AA_MODE_SELECTOR_SLIAA_ID = 0x107AFC5B,
|
||||
ANISO_MODE_LEVEL_ID = 0x101E61A9,
|
||||
ANISO_MODE_SELECTOR_ID = 0x10D2BB16,
|
||||
APPLICATION_PROFILE_NOTIFICATION_TIMEOUT_ID = 0x104554B6,
|
||||
APPLICATION_STEAM_ID_ID = 0x107CDDBC,
|
||||
CPL_HIDDEN_PROFILE_ID = 0x106D5CFF,
|
||||
CUDA_EXCLUDED_GPUS_ID = 0x10354FF8,
|
||||
D3DOGL_GPU_MAX_POWER_ID = 0x10D1EF29,
|
||||
EXPORT_PERF_COUNTERS_ID = 0x108F0841,
|
||||
FXAA_ALLOW_ID = 0x1034CB89,
|
||||
FXAA_ENABLE_ID = 0x1074C972,
|
||||
FXAA_INDICATOR_ENABLE_ID = 0x1068FB9C,
|
||||
MCSFRSHOWSPLIT_ID = 0x10287051,
|
||||
OPTIMUS_MAXAA_ID = 0x10F9DC83,
|
||||
PHYSXINDICATOR_ID = 0x1094F16F,
|
||||
PREFERRED_PSTATE_ID = 0x1057EB71,
|
||||
PREVENT_UI_AF_OVERRIDE_ID = 0x103BCCB5,
|
||||
PS_FRAMERATE_LIMITER_ID = 0x10834FEE,
|
||||
PS_FRAMERATE_LIMITER_GPS_CTRL_ID = 0x10834F01,
|
||||
SHIM_MAXRES_ID = 0x10F9DC82,
|
||||
SHIM_MCCOMPAT_ID = 0x10F9DC80,
|
||||
SHIM_RENDERING_MODE_ID = 0x10F9DC81,
|
||||
SHIM_RENDERING_OPTIONS_ID = 0x10F9DC84,
|
||||
SLI_GPU_COUNT_ID = 0x1033DCD1,
|
||||
SLI_PREDEFINED_GPU_COUNT_ID = 0x1033DCD2,
|
||||
SLI_PREDEFINED_GPU_COUNT_DX10_ID = 0x1033DCD3,
|
||||
SLI_PREDEFINED_MODE_ID = 0x1033CEC1,
|
||||
SLI_PREDEFINED_MODE_DX10_ID = 0x1033CEC2,
|
||||
SLI_RENDERING_MODE_ID = 0x1033CED1,
|
||||
VRRFEATUREINDICATOR_ID = 0x1094F157,
|
||||
VRROVERLAYINDICATOR_ID = 0x1095F16F,
|
||||
VRRREQUESTSTATE_ID = 0x1094F1F7,
|
||||
VSYNCSMOOTHAFR_ID = 0x101AE763,
|
||||
VSYNCVRRCONTROL_ID = 0x10A879CE,
|
||||
VSYNC_BEHAVIOR_FLAGS_ID = 0x10FDEC23,
|
||||
WKS_API_STEREO_EYES_EXCHANGE_ID = 0x11AE435C,
|
||||
WKS_API_STEREO_MODE_ID = 0x11E91A61,
|
||||
WKS_MEMORY_ALLOCATION_POLICY_ID = 0x11112233,
|
||||
WKS_STEREO_DONGLE_SUPPORT_ID = 0x112493BD,
|
||||
WKS_STEREO_SUPPORT_ID = 0x11AA9E99,
|
||||
WKS_STEREO_SWAP_MODE_ID = 0x11333333,
|
||||
AO_MODE_ID = 0x00667329,
|
||||
AO_MODE_ACTIVE_ID = 0x00664339,
|
||||
AUTO_LODBIASADJUST_ID = 0x00638E8F,
|
||||
ICAFE_LOGO_CONFIG_ID = 0x00DB1337,
|
||||
LODBIASADJUST_ID = 0x00738E8F,
|
||||
PRERENDERLIMIT_ID = 0x007BA09E,
|
||||
PS_DYNAMIC_TILING_ID = 0x00E5C6C0,
|
||||
PS_SHADERDISKCACHE_ID = 0x00198FFF,
|
||||
PS_TEXFILTER_ANISO_OPTS2_ID = 0x00E73211,
|
||||
PS_TEXFILTER_BILINEAR_IN_ANISO_ID = 0x0084CD70,
|
||||
PS_TEXFILTER_DISABLE_TRILIN_SLOPE_ID = 0x002ECAF2,
|
||||
PS_TEXFILTER_NO_NEG_LODBIAS_ID = 0x0019BB68,
|
||||
QUALITY_ENHANCEMENTS_ID = 0x00CE2691,
|
||||
REFRESH_RATE_OVERRIDE_ID = 0x0064B541,
|
||||
SET_POWER_THROTTLE_FOR_PCIe_COMPLIANCE_ID = 0x00AE785C,
|
||||
SET_VAB_DATA_ID = 0x00AB8687,
|
||||
VSYNCMODE_ID = 0x00A879CF,
|
||||
VSYNCTEARCONTROL_ID = 0x005A375C,
|
||||
TOTAL_DWORD_SETTING_NUM = 80,
|
||||
TOTAL_WSTRING_SETTING_NUM = 4,
|
||||
TOTAL_SETTING_NUM = 84,
|
||||
INVALID_SETTING_ID = 0xFFFFFFFF
|
||||
}
|
||||
|
||||
internal enum NvShimSetting : uint
|
||||
{
|
||||
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
|
||||
SHIM_RENDERING_MODE_ENABLE = 0x00000001,
|
||||
SHIM_RENDERING_MODE_USER_EDITABLE = 0x00000002,
|
||||
SHIM_RENDERING_MODE_MASK = 0x00000003,
|
||||
SHIM_RENDERING_MODE_VIDEO_MASK = 0x00000004,
|
||||
SHIM_RENDERING_MODE_VARYING_BIT = 0x00000008,
|
||||
SHIM_RENDERING_MODE_AUTO_SELECT = 0x00000010,
|
||||
SHIM_RENDERING_MODE_OVERRIDE_BIT = 0x80000000,
|
||||
SHIM_RENDERING_MODE_NUM_VALUES = 8,
|
||||
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
|
||||
}
|
||||
|
||||
internal enum NvThreadControlSetting : uint
|
||||
{
|
||||
OGL_THREAD_CONTROL_ENABLE = 0x00000001,
|
||||
OGL_THREAD_CONTROL_DISABLE = 0x00000002,
|
||||
OGL_THREAD_CONTROL_NUM_VALUES = 2,
|
||||
OGL_THREAD_CONTROL_DEFAULT = 0
|
||||
}
|
||||
}
|
@ -30,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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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,51 @@ 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.Tracking.ValueChanged += _ => 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)
|
||||
|
@ -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]
|
||||
@ -246,21 +250,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -277,6 +277,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SliderTailHit,
|
||||
HitResult.SmallBonus,
|
||||
HitResult.LargeBonus,
|
||||
};
|
||||
@ -289,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;
|
||||
|
@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -70,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
};
|
||||
|
||||
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
|
||||
userCursorScale.ValueChanged += _ => calculateCursorScale();
|
||||
userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
|
||||
autoCursorScale.ValueChanged += _ => calculateCursorScale();
|
||||
autoCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
|
||||
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
|
||||
}
|
||||
@ -81,10 +81,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
calculateCursorScale();
|
||||
cursorScale.Value = CalculateCursorScale();
|
||||
}
|
||||
|
||||
private void calculateCursorScale()
|
||||
protected virtual float CalculateCursorScale()
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
|
||||
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
|
||||
}
|
||||
|
||||
cursorScale.Value = scale;
|
||||
return scale;
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
|
@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
RelativePositionAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override float CalculateCursorScale()
|
||||
{
|
||||
// Force minimum cursor size so it's easily clickable
|
||||
return Math.Max(1f, base.CalculateCursorScale());
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateColour();
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -310,6 +310,26 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||
}
|
||||
|
||||
[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 +359,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
|
||||
{
|
||||
}
|
||||
|
3
osu.Game.Tests/Resources/mania-skin-broken-array.ini
Normal file
3
osu.Game.Tests/Resources/mania-skin-broken-array.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,,3,3,3
|
@ -84,6 +84,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 +97,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 +169,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 +190,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 +211,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 +232,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 +253,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 +274,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)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
42
osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs
Normal file
42
osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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", () =>
|
||||
{
|
@ -29,6 +29,7 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
@ -690,6 +691,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]
|
||||
|
@ -799,11 +799,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);
|
||||
}
|
||||
|
||||
@ -942,6 +938,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));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/");
|
||||
|
||||
AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page");
|
||||
AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page");
|
||||
|
||||
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
|
||||
@ -275,7 +275,7 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed
|
||||
AddStep("set content", () =>
|
||||
{
|
||||
markdownContainer.Text = @"
|
||||
This is a paragraph containing `inline code` synatax.
|
||||
This is a paragraph containing `inline code` syntax.
|
||||
Oh wow I do love the `WikiMarkdownContainer`, it is very cool!
|
||||
|
||||
This is a line before the fenced code block:
|
||||
|
@ -107,12 +107,12 @@ 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 APIWikiPage responseMainPage => new APIWikiPage
|
||||
{
|
||||
Title = "Main Page",
|
||||
Layout = "main_page",
|
||||
Path = "Main_Page",
|
||||
Title = "Main page",
|
||||
Layout = WikiOverlay.INDEX_PATH.ToLowerInvariant(), // custom classes are always lower snake.
|
||||
Path = WikiOverlay.INDEX_PATH,
|
||||
Locale = "en",
|
||||
Subtitle = null,
|
||||
Markdown =
|
||||
|
@ -0,0 +1,131 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings.Sections.Audio;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
public partial class TestSceneAudioOffsetAdjustControl : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private SessionStatics statics { get; set; } = null!;
|
||||
|
||||
[Cached]
|
||||
private SessionAverageHitErrorTracker tracker = new SessionAverageHitErrorTracker();
|
||||
|
||||
private Container content = null!;
|
||||
protected override Container Content => content;
|
||||
|
||||
private OsuConfigManager localConfig = null!;
|
||||
private AudioOffsetAdjustControl adjustControl = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
localConfig = new OsuConfigManager(LocalStorage);
|
||||
Dependencies.CacheAs(localConfig);
|
||||
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
tracker,
|
||||
content = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = adjustControl = new AudioOffsetAdjustControl
|
||||
{
|
||||
Current = localConfig.GetBindable<double>(OsuSetting.AudioOffset),
|
||||
};
|
||||
|
||||
localConfig.SetValue(OsuSetting.AudioOffset, 0.0);
|
||||
tracker.ClearHistory();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("set new score", () => statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(RNG.NextDouble(-100, 100)),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
}));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBehaviour()
|
||||
{
|
||||
AddStep("set score with -20ms", () => setScore(-20));
|
||||
AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
|
||||
AddStep("set score with 40ms", () => setScore(40));
|
||||
AddAssert("suggested global offset is -40ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-40));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonZeroGlobalOffset()
|
||||
{
|
||||
AddStep("set global offset to -20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, -20.0));
|
||||
AddStep("set score with -20ms", () => setScore(-20));
|
||||
AddAssert("suggested global offset is 0ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(0));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
|
||||
AddStep("set global offset to 20ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 20.0));
|
||||
AddStep("set score with 40ms", () => setScore(40));
|
||||
AddAssert("suggested global offset is -20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(-20));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiplePlays()
|
||||
{
|
||||
AddStep("set score with -20ms", () => setScore(-20));
|
||||
AddStep("set score with -10ms", () => setScore(-10));
|
||||
AddAssert("suggested global offset is 15ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(15));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
|
||||
AddStep("set score with -20ms", () => setScore(-20));
|
||||
AddStep("set global offset to 30ms", () => localConfig.SetValue(OsuSetting.AudioOffset, 30.0));
|
||||
AddStep("set score with 10ms", () => setScore(10));
|
||||
AddAssert("suggested global offset is 20ms", () => adjustControl.SuggestedOffset.Value, () => Is.EqualTo(20));
|
||||
AddStep("clear history", () => tracker.ClearHistory());
|
||||
}
|
||||
|
||||
private void setScore(double averageHitError)
|
||||
{
|
||||
statics.SetValue(Static.LastLocalUserScore, new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(averageHitError),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (localConfig.IsNotNull())
|
||||
localConfig.Dispose();
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
@ -19,11 +20,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
private DialogOverlay overlay;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||
|
||||
TestPopupDialog firstDialog = null;
|
||||
TestPopupDialog secondDialog = null;
|
||||
|
||||
@ -84,7 +89,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}));
|
||||
|
||||
AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog);
|
||||
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
|
||||
AddUntilStep("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooMuchText()
|
||||
{
|
||||
AddStep("dialog #1", () => overlay.Push(new TestPopupDialog
|
||||
{
|
||||
Icon = FontAwesome.Regular.TrashAlt,
|
||||
HeaderText = @"Confirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion ofConfirm deletion of",
|
||||
BodyText = @"Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver.Ayase Rie - Yuima-ru*World TVver. ",
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"I never want to see this again.",
|
||||
Action = () => Console.WriteLine(@"OK"),
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Firetruck, I still want quick ranks!",
|
||||
Action = () => Console.WriteLine(@"Cancel"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -92,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
PopupDialog dialog = null;
|
||||
|
||||
AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
|
||||
AddStep("create slow loading dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
|
||||
|
||||
AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add));
|
||||
|
||||
@ -128,8 +157,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestDismissBeforePush()
|
||||
{
|
||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||
|
||||
TestPopupDialog testDialog = null;
|
||||
AddStep("dismissed dialog push", () =>
|
||||
{
|
||||
@ -146,8 +173,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestDismissBeforePushViaButtonPress()
|
||||
{
|
||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||
|
||||
TestPopupDialog testDialog = null;
|
||||
AddStep("dismissed dialog push", () =>
|
||||
{
|
||||
@ -163,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddAssert("no dialog pushed", () => overlay.CurrentDialog == null);
|
||||
AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null);
|
||||
AddUntilStep("dialog is not part of hierarchy", () => testDialog.Parent == null);
|
||||
}
|
||||
|
||||
private partial class TestPopupDialog : PopupDialog
|
||||
|
@ -9,6 +9,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private bool assertModsMultiplier(IEnumerable<Mod> mods)
|
||||
{
|
||||
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
string expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x";
|
||||
string expectedValue = multiplier == 1 ? string.Empty : ModUtils.FormatScoreMultiplier(multiplier).ToString();
|
||||
|
||||
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
public TestTitle()
|
||||
{
|
||||
Title = "title";
|
||||
Icon = HexaconsIcons.Devtools;
|
||||
Icon = OsuIcon.ChangelogB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
@ -15,24 +12,25 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestScenePopupDialog : OsuManualInputManagerTestScene
|
||||
{
|
||||
private TestPopupDialog dialog;
|
||||
private TestPopupDialog dialog = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("new popup", () =>
|
||||
{
|
||||
Add(dialog = new TestPopupDialog
|
||||
Child = dialog = new TestPopupDialog
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Framework.Graphics.Containers.Visibility.Visible },
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDangerousButton([Values(false, true)] bool atEdge)
|
||||
{
|
||||
AddStep("finish transforms", () => dialog.FinishTransforms(true));
|
||||
|
||||
if (atEdge)
|
||||
{
|
||||
AddStep("move mouse to button edge", () =>
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -28,6 +29,8 @@ namespace osu.Game
|
||||
/// </summary>
|
||||
public partial class BackgroundDataStoreProcessor : Component
|
||||
{
|
||||
protected Task ProcessingTask { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
@ -61,7 +64,7 @@ namespace osu.Game
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
ProcessingTask = Task.Factory.StartNew(() =>
|
||||
{
|
||||
Logger.Log("Beginning background data store processing..");
|
||||
|
||||
@ -314,10 +317,17 @@ namespace osu.Game
|
||||
{
|
||||
Logger.Log("Querying for scores that need total score conversion...");
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
|
||||
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.AsEnumerable().Select(s => s.ID)));
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||
r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed
|
||||
&& s.BeatmapInfo != null
|
||||
&& s.IsLegacyScore
|
||||
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.AsEnumerable()
|
||||
// must be done after materialisation, as realm doesn't want to support
|
||||
// nested property predicates
|
||||
.Where(s => s.Ruleset.IsLegacyRuleset())
|
||||
.Select(s => s.ID)));
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||
|
||||
|
@ -20,6 +20,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@ -96,6 +97,7 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.MenuVoice, true);
|
||||
SetDefault(OsuSetting.MenuMusic, true);
|
||||
SetDefault(OsuSetting.MenuTips, true);
|
||||
|
||||
SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1);
|
||||
|
||||
@ -192,6 +194,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
|
||||
|
||||
SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f);
|
||||
SetDefault<UserStatus?>(OsuSetting.UserOnlineStatus, null);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@ -350,6 +353,7 @@ namespace osu.Game.Configuration
|
||||
VolumeInactive,
|
||||
MenuMusic,
|
||||
MenuVoice,
|
||||
MenuTips,
|
||||
CursorRotation,
|
||||
MenuParallax,
|
||||
Prefer24HourTime,
|
||||
@ -418,5 +422,6 @@ namespace osu.Game.Configuration
|
||||
EditorShowSpeedChanges,
|
||||
TouchDisableGameplayTaps,
|
||||
ModSelectTextSearchStartsActive,
|
||||
UserOnlineStatus,
|
||||
}
|
||||
}
|
||||
|
73
osu.Game/Configuration/SessionAverageHitErrorTracker.cs
Normal file
73
osu.Game/Configuration/SessionAverageHitErrorTracker.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks the local user's average hit error during the ongoing play session.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public partial class SessionAverageHitErrorTracker : Component
|
||||
{
|
||||
public IBindableList<DataPoint> AverageHitErrorHistory => averageHitErrorHistory;
|
||||
private readonly BindableList<DataPoint> averageHitErrorHistory = new BindableList<DataPoint>();
|
||||
|
||||
private readonly Bindable<ScoreInfo?> latestScore = new Bindable<ScoreInfo?>();
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SessionStatics statics)
|
||||
{
|
||||
statics.BindWith(Static.LastLocalUserScore, latestScore);
|
||||
latestScore.BindValueChanged(score => calculateAverageHitError(score.NewValue), true);
|
||||
}
|
||||
|
||||
private void calculateAverageHitError(ScoreInfo? newScore)
|
||||
{
|
||||
if (newScore == null)
|
||||
return;
|
||||
|
||||
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.Count < 10)
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
|
||||
return;
|
||||
|
||||
// keep a sane maximum number of entries.
|
||||
if (averageHitErrorHistory.Count >= 50)
|
||||
averageHitErrorHistory.RemoveAt(0);
|
||||
|
||||
double globalOffset = configManager.Get<double>(OsuSetting.AudioOffset);
|
||||
averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset));
|
||||
}
|
||||
|
||||
public void ClearHistory() => averageHitErrorHistory.Clear();
|
||||
|
||||
public readonly struct DataPoint
|
||||
{
|
||||
public double AverageHitError { get; }
|
||||
public double GlobalAudioOffset { get; }
|
||||
|
||||
public double SuggestedGlobalAudioOffset => GlobalAudioOffset - AverageHitError;
|
||||
|
||||
public DataPoint(double averageHitError, double globalOffset)
|
||||
{
|
||||
AverageHitError = averageHitError;
|
||||
GlobalAudioOffset = globalOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using osu.Game.Input;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@ -27,6 +28,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
|
||||
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
|
||||
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
|
||||
SetDefault<ScoreInfo>(Static.LastLocalUserScore, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -73,5 +75,10 @@ namespace osu.Game.Configuration
|
||||
/// Used in touchscreen detection scenarios (<see cref="TouchInputInterceptor"/>).
|
||||
/// </summary>
|
||||
TouchInputActive,
|
||||
|
||||
/// <summary>
|
||||
/// Stores the local user's last score (can be completed or aborted).
|
||||
/// </summary>
|
||||
LastLocalUserScore,
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
@ -98,15 +98,11 @@ namespace osu.Game.Database
|
||||
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||
realm.Write(r =>
|
||||
{
|
||||
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||
var files = r.All<RealmFile>().ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (var file in r.All<RealmFile>().Filter(@$"{nameof(RealmFile.Usages)}.@count = 0"))
|
||||
{
|
||||
totalFiles++;
|
||||
|
||||
if (file.BacklinksCount > 0)
|
||||
continue;
|
||||
Debug.Assert(file.BacklinksCount == 0);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -311,13 +311,22 @@ namespace osu.Game.Database
|
||||
long maximumLegacyBonusScore = attributes.BonusScore;
|
||||
|
||||
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
// Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0
|
||||
// when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot).
|
||||
// In such cases, just assume 0.
|
||||
double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0
|
||||
? ((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore)
|
||||
: 0;
|
||||
|
||||
double comboProportion;
|
||||
|
||||
if (maximumLegacyComboScore + maximumLegacyBonusScore > 0)
|
||||
{
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
comboProportion = Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Two possible causes:
|
||||
// the beatmap has no bonus objects *AND*
|
||||
// either the active mods have a zero mod multiplier, in which case assume 0,
|
||||
// or the *beatmap* has a zero `difficultyPeppyStars` (or just no combo-giving objects), in which case assume 1.
|
||||
comboProportion = legacyModMultiplier == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
@ -437,16 +446,42 @@ namespace osu.Game.Database
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// compare logic in `CatchScoreProcessor`.
|
||||
|
||||
// this could technically be slightly incorrect in the case of stable scores.
|
||||
// because large droplet misses are counted as full misses in stable scores,
|
||||
// `score.MaximumStatistics.GetValueOrDefault(Great)` will be equal to the count of fruits *and* large droplets
|
||||
// rather than just fruits (which was the intent).
|
||||
// this is not fixable without introducing an extra legacy score attribute dedicated for catch,
|
||||
// and this is a ballpark conversion process anyway, so attempt to trudge on.
|
||||
int fruitTinyScaleDivisor = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + score.MaximumStatistics.GetValueOrDefault(HitResult.Great);
|
||||
double fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
|
||||
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 = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
600000 * comboProportion
|
||||
+ 400000 * score.Accuracy
|
||||
comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// in the mania case accuracy actually changes between score V1 and score V2 / standardised
|
||||
// (PERFECT weighting changes from 300 to 305),
|
||||
// so for better accuracy recompute accuracy locally based on hit statistics and use that instead,
|
||||
double scoreV2Accuracy = ComputeAccuracy(score);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
850000 * comboProportion
|
||||
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||
+ 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
@ -461,6 +496,94 @@ namespace osu.Game.Database
|
||||
return convertedTotalScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless.
|
||||
/// This is because in stable score V1, catch has quadratic score progression,
|
||||
/// while in stable score V2, score progression is logarithmic up to 200 combo and then linear.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map)
|
||||
/// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it.
|
||||
/// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The general idea is that aside from the <paramref name="scoreMaxCombo"/> which the player is known to have hit,
|
||||
/// the remaining misses are evenly distributed across the rest of the objects that give combo.
|
||||
/// This is therefore a worst-case estimate.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static double estimateComboProportionForCatch(int beatmapMaxCombo, int scoreMaxCombo, int scoreMissCount)
|
||||
{
|
||||
if (beatmapMaxCombo == 0)
|
||||
return 1;
|
||||
|
||||
if (scoreMaxCombo == 0)
|
||||
return 0;
|
||||
|
||||
if (beatmapMaxCombo == scoreMaxCombo)
|
||||
return 1;
|
||||
|
||||
double estimatedBestCaseTotal = estimateBestCaseComboTotal(beatmapMaxCombo);
|
||||
|
||||
int remainingCombo = beatmapMaxCombo - (scoreMaxCombo + scoreMissCount);
|
||||
double totalDroppedScore = 0;
|
||||
|
||||
int assumedLengthOfRemainingCombos = (int)Math.Floor((double)remainingCombo / scoreMissCount);
|
||||
|
||||
if (assumedLengthOfRemainingCombos > 0)
|
||||
{
|
||||
int assumedCombosCount = (int)Math.Floor((double)remainingCombo / assumedLengthOfRemainingCombos);
|
||||
totalDroppedScore += assumedCombosCount * estimateDroppedComboScoreAfterMiss(assumedLengthOfRemainingCombos);
|
||||
|
||||
remainingCombo -= assumedCombosCount * assumedLengthOfRemainingCombos;
|
||||
|
||||
if (remainingCombo > 0)
|
||||
totalDroppedScore += estimateDroppedComboScoreAfterMiss(remainingCombo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// there are so many misses that attempting to evenly divide remaining combo results in 0 length per combo,
|
||||
// i.e. all remaining judgements are combo breaks.
|
||||
// in that case, presume every single remaining object is a miss and did not give any combo score.
|
||||
totalDroppedScore = estimatedBestCaseTotal - estimateBestCaseComboTotal(scoreMaxCombo);
|
||||
}
|
||||
|
||||
return estimatedBestCaseTotal == 0
|
||||
? 1
|
||||
: 1 - Math.Clamp(totalDroppedScore / estimatedBestCaseTotal, 0, 1);
|
||||
|
||||
double estimateBestCaseComboTotal(int maxCombo)
|
||||
{
|
||||
if (maxCombo == 0)
|
||||
return 1;
|
||||
|
||||
double estimatedTotal = 0.5 * Math.Min(maxCombo, 2);
|
||||
|
||||
if (maxCombo <= 2)
|
||||
return estimatedTotal;
|
||||
|
||||
// int_2^x log_4(t) dt
|
||||
estimatedTotal += (Math.Min(maxCombo, 200) * (Math.Log(Math.Min(maxCombo, 200)) - 1) + 2 - Math.Log(4)) / Math.Log(4);
|
||||
|
||||
if (maxCombo <= 200)
|
||||
return estimatedTotal;
|
||||
|
||||
estimatedTotal += (maxCombo - 200) * Math.Log(200) / Math.Log(4);
|
||||
return estimatedTotal;
|
||||
}
|
||||
|
||||
double estimateDroppedComboScoreAfterMiss(int lengthOfComboAfterMiss)
|
||||
{
|
||||
if (lengthOfComboAfterMiss >= 200)
|
||||
lengthOfComboAfterMiss = 200;
|
||||
|
||||
// int_0^x (log_4(200) - log_4(t)) dt
|
||||
// note that this is an pessimistic estimate, i.e. it may subtract too much if the miss happened before reaching 200 combo
|
||||
return lengthOfComboAfterMiss * (1 + Math.Log(200) - Math.Log(lengthOfComboAfterMiss)) / Math.Log(4);
|
||||
}
|
||||
}
|
||||
|
||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
|
||||
|
@ -59,7 +59,8 @@ namespace osu.Game.Extensions
|
||||
/// <returns>A short relative string representing the input time.</returns>
|
||||
public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff)
|
||||
{
|
||||
if (time == default)
|
||||
// covers all `DateTimeOffset` instances with the date portion of 0001-01-01.
|
||||
if (time.Date == default)
|
||||
return "-";
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
@ -1,131 +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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Text;
|
||||
|
||||
namespace osu.Game.Graphics
|
||||
{
|
||||
public static class HexaconsIcons
|
||||
{
|
||||
public const string FONT_NAME = "Icons/Hexacons";
|
||||
|
||||
public static IconUsage BeatmapPacks => get(HexaconsMapping.beatmap_packs);
|
||||
public static IconUsage Beatmap => get(HexaconsMapping.beatmap);
|
||||
public static IconUsage Calendar => get(HexaconsMapping.calendar);
|
||||
public static IconUsage Chart => get(HexaconsMapping.chart);
|
||||
public static IconUsage Community => get(HexaconsMapping.community);
|
||||
public static IconUsage Contests => get(HexaconsMapping.contests);
|
||||
public static IconUsage Devtools => get(HexaconsMapping.devtools);
|
||||
public static IconUsage Download => get(HexaconsMapping.download);
|
||||
public static IconUsage Editor => get(HexaconsMapping.editor);
|
||||
public static IconUsage FeaturedArtist => get(HexaconsMapping.featured_artist);
|
||||
public static IconUsage Home => get(HexaconsMapping.home);
|
||||
public static IconUsage Messaging => get(HexaconsMapping.messaging);
|
||||
public static IconUsage Music => get(HexaconsMapping.music);
|
||||
public static IconUsage News => get(HexaconsMapping.news);
|
||||
public static IconUsage Notification => get(HexaconsMapping.notification);
|
||||
public static IconUsage Profile => get(HexaconsMapping.profile);
|
||||
public static IconUsage Rankings => get(HexaconsMapping.rankings);
|
||||
public static IconUsage Search => get(HexaconsMapping.search);
|
||||
public static IconUsage Settings => get(HexaconsMapping.settings);
|
||||
public static IconUsage Social => get(HexaconsMapping.social);
|
||||
public static IconUsage Store => get(HexaconsMapping.store);
|
||||
public static IconUsage Tournament => get(HexaconsMapping.tournament);
|
||||
public static IconUsage Wiki => get(HexaconsMapping.wiki);
|
||||
|
||||
private static IconUsage get(HexaconsMapping icon) => new IconUsage((char)icon, FONT_NAME);
|
||||
|
||||
// Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility.
|
||||
// Names should match filenames in resources.
|
||||
private enum HexaconsMapping
|
||||
{
|
||||
beatmap_packs,
|
||||
beatmap,
|
||||
calendar,
|
||||
chart,
|
||||
community,
|
||||
contests,
|
||||
devtools,
|
||||
download,
|
||||
editor,
|
||||
featured_artist,
|
||||
home,
|
||||
messaging,
|
||||
music,
|
||||
news,
|
||||
notification,
|
||||
profile,
|
||||
rankings,
|
||||
search,
|
||||
settings,
|
||||
social,
|
||||
store,
|
||||
tournament,
|
||||
wiki,
|
||||
}
|
||||
|
||||
public class HexaconsStore : ITextureStore, ITexturedGlyphLookupStore
|
||||
{
|
||||
private readonly TextureStore textures;
|
||||
|
||||
public HexaconsStore(TextureStore textures)
|
||||
{
|
||||
this.textures = textures;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textures.Dispose();
|
||||
}
|
||||
|
||||
public ITexturedCharacterGlyph? Get(string? fontName, char character)
|
||||
{
|
||||
if (fontName == FONT_NAME)
|
||||
return new Glyph(textures.Get($"{fontName}/{((HexaconsMapping)character).ToString().Replace("_", "-")}"));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
|
||||
|
||||
public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
||||
|
||||
public Texture Get(string name) => throw new NotImplementedException();
|
||||
|
||||
public Task<Texture> GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public Stream GetStream(string name) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
|
||||
|
||||
public Task<Texture?> GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public class Glyph : ITexturedCharacterGlyph
|
||||
{
|
||||
public float XOffset => default;
|
||||
public float YOffset => default;
|
||||
public float XAdvance => default;
|
||||
public float Baseline => default;
|
||||
public char Character => default;
|
||||
|
||||
public float GetKerning<T>(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException();
|
||||
|
||||
public Texture Texture { get; }
|
||||
public float Width => Texture.Width;
|
||||
public float Height => Texture.Height;
|
||||
|
||||
public Glyph(Texture texture)
|
||||
{
|
||||
Texture = texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -95,6 +95,7 @@ namespace osu.Game.Graphics
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.Great:
|
||||
return Blue;
|
||||
|
||||
|
@ -1,96 +1,444 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Text;
|
||||
|
||||
namespace osu.Game.Graphics
|
||||
{
|
||||
public static class OsuIcon
|
||||
{
|
||||
public static IconUsage Get(int icon) => new IconUsage((char)icon, "osuFont");
|
||||
#region Legacy spritesheet-based icons
|
||||
|
||||
private static IconUsage get(int icon) => new IconUsage((char)icon, @"osuFont");
|
||||
|
||||
// ruleset icons in circles
|
||||
public static IconUsage RulesetOsu => Get(0xe000);
|
||||
public static IconUsage RulesetMania => Get(0xe001);
|
||||
public static IconUsage RulesetCatch => Get(0xe002);
|
||||
public static IconUsage RulesetTaiko => Get(0xe003);
|
||||
public static IconUsage RulesetOsu => get(0xe000);
|
||||
public static IconUsage RulesetMania => get(0xe001);
|
||||
public static IconUsage RulesetCatch => get(0xe002);
|
||||
public static IconUsage RulesetTaiko => get(0xe003);
|
||||
|
||||
// ruleset icons without circles
|
||||
public static IconUsage FilledCircle => Get(0xe004);
|
||||
public static IconUsage CrossCircle => Get(0xe005);
|
||||
public static IconUsage Logo => Get(0xe006);
|
||||
public static IconUsage ChevronDownCircle => Get(0xe007);
|
||||
public static IconUsage EditCircle => Get(0xe033);
|
||||
public static IconUsage LeftCircle => Get(0xe034);
|
||||
public static IconUsage RightCircle => Get(0xe035);
|
||||
public static IconUsage Charts => Get(0xe036);
|
||||
public static IconUsage Solo => Get(0xe037);
|
||||
public static IconUsage Multi => Get(0xe038);
|
||||
public static IconUsage Gear => Get(0xe039);
|
||||
public static IconUsage FilledCircle => get(0xe004);
|
||||
public static IconUsage Logo => get(0xe006);
|
||||
public static IconUsage ChevronDownCircle => get(0xe007);
|
||||
public static IconUsage EditCircle => get(0xe033);
|
||||
public static IconUsage LeftCircle => get(0xe034);
|
||||
public static IconUsage RightCircle => get(0xe035);
|
||||
public static IconUsage Charts => get(0xe036);
|
||||
public static IconUsage Solo => get(0xe037);
|
||||
public static IconUsage Multi => get(0xe038);
|
||||
public static IconUsage Gear => get(0xe039);
|
||||
|
||||
// misc icons
|
||||
public static IconUsage Bat => Get(0xe008);
|
||||
public static IconUsage Bubble => Get(0xe009);
|
||||
public static IconUsage BubblePop => Get(0xe02e);
|
||||
public static IconUsage Dice => Get(0xe011);
|
||||
public static IconUsage Heart => Get(0xe02f);
|
||||
public static IconUsage HeartBreak => Get(0xe030);
|
||||
public static IconUsage Hot => Get(0xe031);
|
||||
public static IconUsage ListSearch => Get(0xe032);
|
||||
public static IconUsage Bat => get(0xe008);
|
||||
public static IconUsage Bubble => get(0xe009);
|
||||
public static IconUsage BubblePop => get(0xe02e);
|
||||
public static IconUsage Dice => get(0xe011);
|
||||
public static IconUsage HeartBreak => get(0xe030);
|
||||
public static IconUsage Hot => get(0xe031);
|
||||
public static IconUsage ListSearch => get(0xe032);
|
||||
|
||||
//osu! playstyles
|
||||
public static IconUsage PlayStyleTablet => Get(0xe02a);
|
||||
public static IconUsage PlayStyleMouse => Get(0xe029);
|
||||
public static IconUsage PlayStyleKeyboard => Get(0xe02b);
|
||||
public static IconUsage PlayStyleTouch => Get(0xe02c);
|
||||
public static IconUsage PlayStyleTablet => get(0xe02a);
|
||||
public static IconUsage PlayStyleMouse => get(0xe029);
|
||||
public static IconUsage PlayStyleKeyboard => get(0xe02b);
|
||||
public static IconUsage PlayStyleTouch => get(0xe02c);
|
||||
|
||||
// osu! difficulties
|
||||
public static IconUsage EasyOsu => Get(0xe015);
|
||||
public static IconUsage NormalOsu => Get(0xe016);
|
||||
public static IconUsage HardOsu => Get(0xe017);
|
||||
public static IconUsage InsaneOsu => Get(0xe018);
|
||||
public static IconUsage ExpertOsu => Get(0xe019);
|
||||
public static IconUsage EasyOsu => get(0xe015);
|
||||
public static IconUsage NormalOsu => get(0xe016);
|
||||
public static IconUsage HardOsu => get(0xe017);
|
||||
public static IconUsage InsaneOsu => get(0xe018);
|
||||
public static IconUsage ExpertOsu => get(0xe019);
|
||||
|
||||
// taiko difficulties
|
||||
public static IconUsage EasyTaiko => Get(0xe01a);
|
||||
public static IconUsage NormalTaiko => Get(0xe01b);
|
||||
public static IconUsage HardTaiko => Get(0xe01c);
|
||||
public static IconUsage InsaneTaiko => Get(0xe01d);
|
||||
public static IconUsage ExpertTaiko => Get(0xe01e);
|
||||
public static IconUsage EasyTaiko => get(0xe01a);
|
||||
public static IconUsage NormalTaiko => get(0xe01b);
|
||||
public static IconUsage HardTaiko => get(0xe01c);
|
||||
public static IconUsage InsaneTaiko => get(0xe01d);
|
||||
public static IconUsage ExpertTaiko => get(0xe01e);
|
||||
|
||||
// fruits difficulties
|
||||
public static IconUsage EasyFruits => Get(0xe01f);
|
||||
public static IconUsage NormalFruits => Get(0xe020);
|
||||
public static IconUsage HardFruits => Get(0xe021);
|
||||
public static IconUsage InsaneFruits => Get(0xe022);
|
||||
public static IconUsage ExpertFruits => Get(0xe023);
|
||||
public static IconUsage EasyFruits => get(0xe01f);
|
||||
public static IconUsage NormalFruits => get(0xe020);
|
||||
public static IconUsage HardFruits => get(0xe021);
|
||||
public static IconUsage InsaneFruits => get(0xe022);
|
||||
public static IconUsage ExpertFruits => get(0xe023);
|
||||
|
||||
// mania difficulties
|
||||
public static IconUsage EasyMania => Get(0xe024);
|
||||
public static IconUsage NormalMania => Get(0xe025);
|
||||
public static IconUsage HardMania => Get(0xe026);
|
||||
public static IconUsage InsaneMania => Get(0xe027);
|
||||
public static IconUsage ExpertMania => Get(0xe028);
|
||||
public static IconUsage EasyMania => get(0xe024);
|
||||
public static IconUsage NormalMania => get(0xe025);
|
||||
public static IconUsage HardMania => get(0xe026);
|
||||
public static IconUsage InsaneMania => get(0xe027);
|
||||
public static IconUsage ExpertMania => get(0xe028);
|
||||
|
||||
// mod icons
|
||||
public static IconUsage ModPerfect => Get(0xe049);
|
||||
public static IconUsage ModAutopilot => Get(0xe03a);
|
||||
public static IconUsage ModAuto => Get(0xe03b);
|
||||
public static IconUsage ModCinema => Get(0xe03c);
|
||||
public static IconUsage ModDoubleTime => Get(0xe03d);
|
||||
public static IconUsage ModEasy => Get(0xe03e);
|
||||
public static IconUsage ModFlashlight => Get(0xe03f);
|
||||
public static IconUsage ModHalftime => Get(0xe040);
|
||||
public static IconUsage ModHardRock => Get(0xe041);
|
||||
public static IconUsage ModHidden => Get(0xe042);
|
||||
public static IconUsage ModNightcore => Get(0xe043);
|
||||
public static IconUsage ModNoFail => Get(0xe044);
|
||||
public static IconUsage ModRelax => Get(0xe045);
|
||||
public static IconUsage ModSpunOut => Get(0xe046);
|
||||
public static IconUsage ModSuddenDeath => Get(0xe047);
|
||||
public static IconUsage ModTarget => Get(0xe048);
|
||||
public static IconUsage ModPerfect => get(0xe049);
|
||||
public static IconUsage ModAutopilot => get(0xe03a);
|
||||
public static IconUsage ModAuto => get(0xe03b);
|
||||
public static IconUsage ModCinema => get(0xe03c);
|
||||
public static IconUsage ModDoubleTime => get(0xe03d);
|
||||
public static IconUsage ModEasy => get(0xe03e);
|
||||
public static IconUsage ModFlashlight => get(0xe03f);
|
||||
public static IconUsage ModHalftime => get(0xe040);
|
||||
public static IconUsage ModHardRock => get(0xe041);
|
||||
public static IconUsage ModHidden => get(0xe042);
|
||||
public static IconUsage ModNightcore => get(0xe043);
|
||||
public static IconUsage ModNoFail => get(0xe044);
|
||||
public static IconUsage ModRelax => get(0xe045);
|
||||
public static IconUsage ModSpunOut => get(0xe046);
|
||||
public static IconUsage ModSuddenDeath => get(0xe047);
|
||||
public static IconUsage ModTarget => get(0xe048);
|
||||
|
||||
// Use "Icons/BeatmapDetails/mod-icon" instead
|
||||
// public static IconUsage ModBg => Get(0xe04a);
|
||||
|
||||
#endregion
|
||||
|
||||
#region New single-file-based icons
|
||||
|
||||
public const string FONT_NAME = @"Icons";
|
||||
|
||||
public static IconUsage Audio => get(OsuIconMapping.Audio);
|
||||
public static IconUsage Beatmap => get(OsuIconMapping.Beatmap);
|
||||
public static IconUsage Calendar => get(OsuIconMapping.Calendar);
|
||||
public static IconUsage ChangelogA => get(OsuIconMapping.ChangelogA);
|
||||
public static IconUsage ChangelogB => get(OsuIconMapping.ChangelogB);
|
||||
public static IconUsage Chat => get(OsuIconMapping.Chat);
|
||||
public static IconUsage CheckCircle => get(OsuIconMapping.CheckCircle);
|
||||
public static IconUsage CollapseA => get(OsuIconMapping.CollapseA);
|
||||
public static IconUsage Collections => get(OsuIconMapping.Collections);
|
||||
public static IconUsage Cross => get(OsuIconMapping.Cross);
|
||||
public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle);
|
||||
public static IconUsage Crown => get(OsuIconMapping.Crown);
|
||||
public static IconUsage Debug => get(OsuIconMapping.Debug);
|
||||
public static IconUsage Delete => get(OsuIconMapping.Delete);
|
||||
public static IconUsage Details => get(OsuIconMapping.Details);
|
||||
public static IconUsage Discord => get(OsuIconMapping.Discord);
|
||||
public static IconUsage EllipsisHorizontal => get(OsuIconMapping.EllipsisHorizontal);
|
||||
public static IconUsage EllipsisVertical => get(OsuIconMapping.EllipsisVertical);
|
||||
public static IconUsage ExpandA => get(OsuIconMapping.ExpandA);
|
||||
public static IconUsage ExpandB => get(OsuIconMapping.ExpandB);
|
||||
public static IconUsage FeaturedArtist => get(OsuIconMapping.FeaturedArtist);
|
||||
public static IconUsage FeaturedArtistCircle => get(OsuIconMapping.FeaturedArtistCircle);
|
||||
public static IconUsage GameplayA => get(OsuIconMapping.GameplayA);
|
||||
public static IconUsage GameplayB => get(OsuIconMapping.GameplayB);
|
||||
public static IconUsage GameplayC => get(OsuIconMapping.GameplayC);
|
||||
public static IconUsage Global => get(OsuIconMapping.Global);
|
||||
public static IconUsage Graphics => get(OsuIconMapping.Graphics);
|
||||
public static IconUsage Heart => get(OsuIconMapping.Heart);
|
||||
public static IconUsage Home => get(OsuIconMapping.Home);
|
||||
public static IconUsage Input => get(OsuIconMapping.Input);
|
||||
public static IconUsage Maintenance => get(OsuIconMapping.Maintenance);
|
||||
public static IconUsage Megaphone => get(OsuIconMapping.Megaphone);
|
||||
public static IconUsage Music => get(OsuIconMapping.Music);
|
||||
public static IconUsage News => get(OsuIconMapping.News);
|
||||
public static IconUsage Next => get(OsuIconMapping.Next);
|
||||
public static IconUsage NextCircle => get(OsuIconMapping.NextCircle);
|
||||
public static IconUsage Notification => get(OsuIconMapping.Notification);
|
||||
public static IconUsage Online => get(OsuIconMapping.Online);
|
||||
public static IconUsage Play => get(OsuIconMapping.Play);
|
||||
public static IconUsage Player => get(OsuIconMapping.Player);
|
||||
public static IconUsage PlayerFollow => get(OsuIconMapping.PlayerFollow);
|
||||
public static IconUsage Prev => get(OsuIconMapping.Prev);
|
||||
public static IconUsage PrevCircle => get(OsuIconMapping.PrevCircle);
|
||||
public static IconUsage Ranking => get(OsuIconMapping.Ranking);
|
||||
public static IconUsage Rulesets => get(OsuIconMapping.Rulesets);
|
||||
public static IconUsage Search => get(OsuIconMapping.Search);
|
||||
public static IconUsage Settings => get(OsuIconMapping.Settings);
|
||||
public static IconUsage SkinA => get(OsuIconMapping.SkinA);
|
||||
public static IconUsage SkinB => get(OsuIconMapping.SkinB);
|
||||
public static IconUsage Star => get(OsuIconMapping.Star);
|
||||
public static IconUsage Storyboard => get(OsuIconMapping.Storyboard);
|
||||
public static IconUsage Team => get(OsuIconMapping.Team);
|
||||
public static IconUsage ThumbsUp => get(OsuIconMapping.ThumbsUp);
|
||||
public static IconUsage Tournament => get(OsuIconMapping.Tournament);
|
||||
public static IconUsage Twitter => get(OsuIconMapping.Twitter);
|
||||
public static IconUsage UserInterface => get(OsuIconMapping.UserInterface);
|
||||
public static IconUsage Wiki => get(OsuIconMapping.Wiki);
|
||||
public static IconUsage EditorAddControlPoint => get(OsuIconMapping.EditorAddControlPoint);
|
||||
public static IconUsage EditorConvertToStream => get(OsuIconMapping.EditorConvertToStream);
|
||||
public static IconUsage EditorDistanceSnap => get(OsuIconMapping.EditorDistanceSnap);
|
||||
public static IconUsage EditorFinish => get(OsuIconMapping.EditorFinish);
|
||||
public static IconUsage EditorGridSnap => get(OsuIconMapping.EditorGridSnap);
|
||||
public static IconUsage EditorNewComboA => get(OsuIconMapping.EditorNewComboA);
|
||||
public static IconUsage EditorNewComboB => get(OsuIconMapping.EditorNewComboB);
|
||||
public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect);
|
||||
public static IconUsage EditorSound => get(OsuIconMapping.EditorSound);
|
||||
public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle);
|
||||
|
||||
private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME);
|
||||
|
||||
private enum OsuIconMapping
|
||||
{
|
||||
[Description(@"audio")]
|
||||
Audio,
|
||||
|
||||
[Description(@"beatmap")]
|
||||
Beatmap,
|
||||
|
||||
[Description(@"calendar")]
|
||||
Calendar,
|
||||
|
||||
[Description(@"changelog-a")]
|
||||
ChangelogA,
|
||||
|
||||
[Description(@"changelog-b")]
|
||||
ChangelogB,
|
||||
|
||||
[Description(@"chat")]
|
||||
Chat,
|
||||
|
||||
[Description(@"check-circle")]
|
||||
CheckCircle,
|
||||
|
||||
[Description(@"collapse-a")]
|
||||
CollapseA,
|
||||
|
||||
[Description(@"collections")]
|
||||
Collections,
|
||||
|
||||
[Description(@"cross")]
|
||||
Cross,
|
||||
|
||||
[Description(@"cross-circle")]
|
||||
CrossCircle,
|
||||
|
||||
[Description(@"crown")]
|
||||
Crown,
|
||||
|
||||
[Description(@"debug")]
|
||||
Debug,
|
||||
|
||||
[Description(@"delete")]
|
||||
Delete,
|
||||
|
||||
[Description(@"details")]
|
||||
Details,
|
||||
|
||||
[Description(@"discord")]
|
||||
Discord,
|
||||
|
||||
[Description(@"ellipsis-horizontal")]
|
||||
EllipsisHorizontal,
|
||||
|
||||
[Description(@"ellipsis-vertical")]
|
||||
EllipsisVertical,
|
||||
|
||||
[Description(@"expand-a")]
|
||||
ExpandA,
|
||||
|
||||
[Description(@"expand-b")]
|
||||
ExpandB,
|
||||
|
||||
[Description(@"featured-artist")]
|
||||
FeaturedArtist,
|
||||
|
||||
[Description(@"featured-artist-circle")]
|
||||
FeaturedArtistCircle,
|
||||
|
||||
[Description(@"gameplay-a")]
|
||||
GameplayA,
|
||||
|
||||
[Description(@"gameplay-b")]
|
||||
GameplayB,
|
||||
|
||||
[Description(@"gameplay-c")]
|
||||
GameplayC,
|
||||
|
||||
[Description(@"global")]
|
||||
Global,
|
||||
|
||||
[Description(@"graphics")]
|
||||
Graphics,
|
||||
|
||||
[Description(@"heart")]
|
||||
Heart,
|
||||
|
||||
[Description(@"home")]
|
||||
Home,
|
||||
|
||||
[Description(@"input")]
|
||||
Input,
|
||||
|
||||
[Description(@"maintenance")]
|
||||
Maintenance,
|
||||
|
||||
[Description(@"megaphone")]
|
||||
Megaphone,
|
||||
|
||||
[Description(@"music")]
|
||||
Music,
|
||||
|
||||
[Description(@"news")]
|
||||
News,
|
||||
|
||||
[Description(@"next")]
|
||||
Next,
|
||||
|
||||
[Description(@"next-circle")]
|
||||
NextCircle,
|
||||
|
||||
[Description(@"notification")]
|
||||
Notification,
|
||||
|
||||
[Description(@"online")]
|
||||
Online,
|
||||
|
||||
[Description(@"play")]
|
||||
Play,
|
||||
|
||||
[Description(@"player")]
|
||||
Player,
|
||||
|
||||
[Description(@"player-follow")]
|
||||
PlayerFollow,
|
||||
|
||||
[Description(@"prev")]
|
||||
Prev,
|
||||
|
||||
[Description(@"prev-circle")]
|
||||
PrevCircle,
|
||||
|
||||
[Description(@"ranking")]
|
||||
Ranking,
|
||||
|
||||
[Description(@"rulesets")]
|
||||
Rulesets,
|
||||
|
||||
[Description(@"search")]
|
||||
Search,
|
||||
|
||||
[Description(@"settings")]
|
||||
Settings,
|
||||
|
||||
[Description(@"skin-a")]
|
||||
SkinA,
|
||||
|
||||
[Description(@"skin-b")]
|
||||
SkinB,
|
||||
|
||||
[Description(@"star")]
|
||||
Star,
|
||||
|
||||
[Description(@"storyboard")]
|
||||
Storyboard,
|
||||
|
||||
[Description(@"team")]
|
||||
Team,
|
||||
|
||||
[Description(@"thumbs-up")]
|
||||
ThumbsUp,
|
||||
|
||||
[Description(@"tournament")]
|
||||
Tournament,
|
||||
|
||||
[Description(@"twitter")]
|
||||
Twitter,
|
||||
|
||||
[Description(@"user-interface")]
|
||||
UserInterface,
|
||||
|
||||
[Description(@"wiki")]
|
||||
Wiki,
|
||||
|
||||
[Description(@"Editor/add-control-point")]
|
||||
EditorAddControlPoint = 1000,
|
||||
|
||||
[Description(@"Editor/convert-to-stream")]
|
||||
EditorConvertToStream,
|
||||
|
||||
[Description(@"Editor/distance-snap")]
|
||||
EditorDistanceSnap,
|
||||
|
||||
[Description(@"Editor/finish")]
|
||||
EditorFinish,
|
||||
|
||||
[Description(@"Editor/grid-snap")]
|
||||
EditorGridSnap,
|
||||
|
||||
[Description(@"Editor/new-combo-a")]
|
||||
EditorNewComboA,
|
||||
|
||||
[Description(@"Editor/new-combo-b")]
|
||||
EditorNewComboB,
|
||||
|
||||
[Description(@"Editor/select")]
|
||||
EditorSelect,
|
||||
|
||||
[Description(@"Editor/sound")]
|
||||
EditorSound,
|
||||
|
||||
[Description(@"Editor/whistle")]
|
||||
EditorWhistle,
|
||||
}
|
||||
|
||||
public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore
|
||||
{
|
||||
private readonly TextureStore textures;
|
||||
|
||||
public OsuIconStore(TextureStore textures)
|
||||
{
|
||||
this.textures = textures;
|
||||
}
|
||||
|
||||
public ITexturedCharacterGlyph? Get(string? fontName, char character)
|
||||
{
|
||||
if (fontName == FONT_NAME)
|
||||
return new Glyph(textures.Get($@"{fontName}/{((OsuIconMapping)character).GetDescription()}"));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
|
||||
|
||||
public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
||||
|
||||
public Texture Get(string name) => throw new NotImplementedException();
|
||||
|
||||
public Task<Texture> GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public Stream GetStream(string name) => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
|
||||
|
||||
public Task<Texture?> GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException();
|
||||
|
||||
public class Glyph : ITexturedCharacterGlyph
|
||||
{
|
||||
public float XOffset => default;
|
||||
public float YOffset => default;
|
||||
public float XAdvance => default;
|
||||
public float Baseline => default;
|
||||
public char Character => default;
|
||||
|
||||
public float GetKerning<T>(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException();
|
||||
|
||||
public Texture Texture { get; }
|
||||
public float Width => Texture.Width;
|
||||
public float Height => Texture.Height;
|
||||
|
||||
public Glyph(Texture texture)
|
||||
{
|
||||
Texture = texture;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textures.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private const float idle_width = 0.8f;
|
||||
private const float hover_width = 0.9f;
|
||||
|
||||
private const float hover_duration = 500;
|
||||
private const float hover_duration = 300;
|
||||
private const float click_duration = 200;
|
||||
|
||||
public event Action<SelectionState>? StateChanged;
|
||||
@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private readonly Box rightGlow;
|
||||
private readonly Box background;
|
||||
private readonly SpriteText spriteText;
|
||||
private Vector2 hoverSpacing => new Vector2(3f, 0f);
|
||||
private Vector2 hoverSpacing => new Vector2(1.4f, 0f);
|
||||
|
||||
public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
|
||||
: base(sampleSet)
|
||||
@ -279,15 +279,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (newState == SelectionState.Selected)
|
||||
{
|
||||
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
|
||||
ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
|
||||
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutQuint);
|
||||
ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutQuint);
|
||||
glowContainer.FadeIn(hover_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
|
||||
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
|
||||
glowContainer.FadeOut(hover_duration, Easing.OutQuint);
|
||||
ColourContainer.ResizeWidthTo(idle_width, hover_duration / 2, Easing.OutQuint);
|
||||
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration / 2, Easing.OutQuint);
|
||||
glowContainer.FadeOut(hover_duration / 2, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -363,6 +363,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.LoadComplete();
|
||||
|
||||
SearchBar.State.ValueChanged += _ => updateColour();
|
||||
Enabled.BindValueChanged(_ => updateColour());
|
||||
updateColour();
|
||||
}
|
||||
|
||||
@ -383,6 +384,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||
var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||
|
||||
Colour = Color4.White;
|
||||
Alpha = Enabled.Value ? 1 : 0.3f;
|
||||
|
||||
if (SearchBar.State.Value == Visibility.Visible)
|
||||
{
|
||||
Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
|
||||
|
@ -20,9 +20,14 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "Invite player"
|
||||
/// "Invite to room"
|
||||
/// </summary>
|
||||
public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player");
|
||||
public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite to room");
|
||||
|
||||
/// <summary>
|
||||
/// "Spectate"
|
||||
/// </summary>
|
||||
public static LocalisableString SpectatePlayer => new TranslatableString(getKey(@"spectate_player"), @"Spectate");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString ChangelogDescription => new TranslatableString(getKey(@"changelog_description"), @"track recent dev updates in the osu! ecosystem");
|
||||
|
||||
/// <summary>
|
||||
/// "view your friends and other information"
|
||||
/// "view your friends and spectate other players"
|
||||
/// </summary>
|
||||
public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and other information");
|
||||
public static LocalisableString DashboardDescription => new TranslatableString(getKey(@"dashboard_description"), @"view your friends and spectate other players");
|
||||
|
||||
/// <summary>
|
||||
/// "find out who's the best right now"
|
||||
/// "find out who's the best right now"
|
||||
/// </summary>
|
||||
public static LocalisableString RankingsDescription => new TranslatableString(getKey(@"rankings_description"), @"find out who's the best right now");
|
||||
|
||||
@ -39,6 +39,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString WikiDescription => new TranslatableString(getKey(@"wiki_description"), @"knowledge base");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString MenuCursorSize => new TranslatableString(getKey(@"menu_cursor_size"), @"Menu cursor size");
|
||||
|
||||
/// <summary>
|
||||
/// "Menu tips"
|
||||
/// </summary>
|
||||
public static LocalisableString ShowMenuTips => new TranslatableString(getKey(@"show_menu_tips"), @"Menu tips");
|
||||
|
||||
/// <summary>
|
||||
/// "Parallax"
|
||||
/// </summary>
|
||||
@ -154,6 +159,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -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.Linq;
|
||||
using osu.Game.IO;
|
||||
using Realms;
|
||||
|
||||
@ -11,5 +12,8 @@ namespace osu.Game.Models
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
|
||||
[Backlink(nameof(RealmNamedFileUsage.File))]
|
||||
public IQueryable<RealmNamedFileUsage> Usages { get; } = null!;
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,9 @@ namespace osu.Game.Online.API
|
||||
|
||||
private Bindable<UserActivity> activity { get; } = new Bindable<UserActivity>();
|
||||
|
||||
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
|
||||
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
|
||||
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
@ -85,12 +88,20 @@ namespace osu.Game.Online.API
|
||||
authentication.TokenString = config.Get<string>(OsuSetting.Token);
|
||||
authentication.Token.ValueChanged += onTokenChanged;
|
||||
|
||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||
|
||||
localUser.BindValueChanged(u =>
|
||||
{
|
||||
u.OldValue?.Activity.UnbindFrom(activity);
|
||||
u.NewValue.Activity.BindTo(activity);
|
||||
|
||||
if (u.OldValue != null)
|
||||
localUserStatus.UnbindFrom(u.OldValue.Status);
|
||||
localUserStatus.BindTo(u.NewValue.Status);
|
||||
}, true);
|
||||
|
||||
localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue);
|
||||
|
||||
var thread = new Thread(run)
|
||||
{
|
||||
Name = "APIAccess",
|
||||
@ -200,6 +211,7 @@ namespace osu.Game.Online.API
|
||||
setLocalUser(new APIUser
|
||||
{
|
||||
Username = ProvidedUsername,
|
||||
Status = { Value = configStatus.Value ?? UserStatus.Online }
|
||||
});
|
||||
}
|
||||
|
||||
@ -246,8 +258,7 @@ namespace osu.Game.Online.API
|
||||
};
|
||||
userReq.Success += user =>
|
||||
{
|
||||
// todo: save/pull from settings
|
||||
user.Status.Value = UserStatus.Online;
|
||||
user.Status.Value = configStatus.Value ?? UserStatus.Online;
|
||||
|
||||
setLocalUser(user);
|
||||
|
||||
|
16
osu.Game/Online/API/Requests/GetSystemTitleRequest.cs
Normal file
16
osu.Game/Online/API/Requests/GetSystemTitleRequest.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetSystemTitleRequest : OsuJsonWebRequest<APISystemTitle>
|
||||
{
|
||||
public GetSystemTitleRequest()
|
||||
: base($@"https://assets.ppy.sh/lazer-status.json?{DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 1800}")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
30
osu.Game/Online/API/Requests/Responses/APISystemTitle.cs
Normal file
30
osu.Game/Online/API/Requests/Responses/APISystemTitle.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APISystemTitle : IEquatable<APISystemTitle>
|
||||
{
|
||||
[JsonProperty(@"image")]
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty(@"url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public bool Equals(APISystemTitle? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return Image == other.Image && Url == other.Url;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other);
|
||||
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
public override int GetHashCode() => HashCode.Combine(Image, Url);
|
||||
}
|
||||
}
|
@ -247,7 +247,7 @@ namespace osu.Game.Online.Chat
|
||||
string command = parameters[0];
|
||||
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
|
||||
|
||||
switch (command)
|
||||
switch (command.ToLowerInvariant())
|
||||
{
|
||||
case "np":
|
||||
AddInternal(new NowPlayingCommand(target));
|
||||
|
@ -152,6 +152,15 @@ namespace osu.Game.Online.Leaderboards
|
||||
/// </summary>
|
||||
public void RefetchScores() => Scheduler.AddOnce(refetchScores);
|
||||
|
||||
/// <summary>
|
||||
/// Clear all scores from the display.
|
||||
/// </summary>
|
||||
public void ClearScores()
|
||||
{
|
||||
cancelPendingWork();
|
||||
SetScores(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call when a retrieval or display failure happened to show a relevant message to the user.
|
||||
/// </summary>
|
||||
@ -220,9 +229,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
cancelPendingWork();
|
||||
|
||||
SetScores(null);
|
||||
ClearScores();
|
||||
setState(LeaderboardState.Retrieving);
|
||||
|
||||
currentFetchCancellationSource = new CancellationTokenSource();
|
||||
|
@ -23,7 +23,6 @@ namespace osu.Game.Online.Metadata
|
||||
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
||||
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
||||
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
||||
|
||||
@ -192,7 +191,7 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (presence != null)
|
||||
if (presence?.Status != null)
|
||||
userStates[userId] = presence.Value;
|
||||
else
|
||||
userStates.Remove(userId);
|
||||
|
@ -994,8 +994,11 @@ namespace osu.Game
|
||||
Margin = new MarginPadding(5),
|
||||
}, topMostOverlayContent.Add);
|
||||
|
||||
if (!args?.Any(a => a == @"--no-version-overlay") ?? true)
|
||||
loadComponentSingleFile(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
|
||||
if (!IsDeployedBuild)
|
||||
{
|
||||
dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue });
|
||||
loadComponentSingleFile(versionManager, ScreenContainer.Add);
|
||||
}
|
||||
|
||||
loadComponentSingleFile(osuLogo, _ =>
|
||||
{
|
||||
|
@ -200,6 +200,8 @@ namespace osu.Game
|
||||
|
||||
private RulesetConfigCache rulesetConfigCache;
|
||||
|
||||
private SessionAverageHitErrorTracker hitErrorTracker;
|
||||
|
||||
protected SpectatorClient SpectatorClient { get; private set; }
|
||||
|
||||
protected MultiplayerClient MultiplayerClient { get; private set; }
|
||||
@ -349,6 +351,7 @@ namespace osu.Game
|
||||
dependencies.CacheAs(powerStatus);
|
||||
|
||||
dependencies.Cache(SessionStatics = new SessionStatics());
|
||||
dependencies.Cache(hitErrorTracker = new SessionAverageHitErrorTracker());
|
||||
dependencies.Cache(Colours = new OsuColour());
|
||||
|
||||
RegisterImportHandler(BeatmapManager);
|
||||
@ -408,6 +411,7 @@ namespace osu.Game
|
||||
});
|
||||
|
||||
base.Content.Add(new TouchInputInterceptor());
|
||||
base.Content.Add(hitErrorTracker);
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
|
||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||
@ -478,7 +482,7 @@ namespace osu.Game
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||
|
||||
Fonts.AddStore(new HexaconsIcons.HexaconsStore(Textures));
|
||||
Fonts.AddStore(new OsuIcon.OsuIconStore(Textures));
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
Title = PageTitleStrings.MainBeatmapsetsControllerIndex;
|
||||
Description = NamedOverlayComponentStrings.BeatmapListingDescription;
|
||||
Icon = HexaconsIcons.Beatmap;
|
||||
Icon = OsuIcon.Beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
public BeatmapHeaderTitle()
|
||||
{
|
||||
Title = PageTitleStrings.MainBeatmapsetsControllerShow;
|
||||
Icon = HexaconsIcons.Beatmap;
|
||||
Icon = OsuIcon.Beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
{
|
||||
Title = PageTitleStrings.MainChangelogControllerDefault;
|
||||
Description = NamedOverlayComponentStrings.ChangelogDescription;
|
||||
Icon = HexaconsIcons.Devtools;
|
||||
Icon = OsuIcon.ChangelogB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Icon = HexaconsIcons.Messaging,
|
||||
Icon = OsuIcon.Chat,
|
||||
Size = new Vector2(24),
|
||||
},
|
||||
// Placeholder text
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
public IconUsage Icon => HexaconsIcons.Messaging;
|
||||
public IconUsage Icon => OsuIcon.Chat;
|
||||
public LocalisableString Title => ChatStrings.HeaderTitle;
|
||||
public LocalisableString Description => ChatStrings.HeaderDescription;
|
||||
|
||||
|
@ -131,9 +131,6 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
int userId = kvp.Key;
|
||||
|
||||
if (userId == api.LocalUser.Value.Id)
|
||||
continue;
|
||||
|
||||
users.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
APIUser user = task.GetResultSafely();
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
Title = PageTitleStrings.MainHomeControllerIndex;
|
||||
Description = NamedOverlayComponentStrings.DashboardDescription;
|
||||
Icon = HexaconsIcons.Social;
|
||||
Icon = OsuIcon.Global;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
@ -25,14 +26,13 @@ namespace osu.Game.Overlays.Dialog
|
||||
public abstract partial class PopupDialog : VisibilityContainer
|
||||
{
|
||||
public const float ENTER_DURATION = 500;
|
||||
public const float EXIT_DURATION = 200;
|
||||
public const float EXIT_DURATION = 500;
|
||||
|
||||
private readonly Vector2 ringSize = new Vector2(100f);
|
||||
private readonly Vector2 ringMinifiedSize = new Vector2(20f);
|
||||
private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f);
|
||||
|
||||
private readonly Box flashLayer;
|
||||
private Sample flashSample = null!;
|
||||
private Sample? flashSample;
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Container ring;
|
||||
@ -108,13 +108,20 @@ namespace osu.Game.Overlays.Dialog
|
||||
|
||||
protected PopupDialog()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Alpha = 0f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -122,11 +129,13 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 20,
|
||||
CornerExponent = 2.5f,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
Radius = 8,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
Radius = 14,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -142,23 +151,29 @@ namespace osu.Game.Overlays.Dialog
|
||||
ColourDark = Color4Extensions.FromHex(@"1e171e"),
|
||||
TriangleScale = 4,
|
||||
},
|
||||
flashLayer = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Color4Extensions.FromHex(@"221a21"),
|
||||
},
|
||||
},
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Padding = new MarginPadding { Bottom = 10 },
|
||||
Padding = new MarginPadding { Vertical = 60 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Padding = new MarginPadding { Bottom = 30 },
|
||||
Size = ringSize,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -181,6 +196,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Icon = FontAwesome.Solid.TimesCircle,
|
||||
Y = -2,
|
||||
Size = new Vector2(50),
|
||||
},
|
||||
},
|
||||
@ -194,6 +210,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
Padding = new MarginPadding { Horizontal = 5 },
|
||||
},
|
||||
body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18))
|
||||
{
|
||||
@ -202,25 +219,19 @@ namespace osu.Game.Overlays.Dialog
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(5),
|
||||
Padding = new MarginPadding { Horizontal = 5 },
|
||||
},
|
||||
buttonsContainer = new FillFlowContainer<PopupDialogButton>
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Top = 30 },
|
||||
},
|
||||
},
|
||||
},
|
||||
buttonsContainer = new FillFlowContainer<PopupDialogButton>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
},
|
||||
flashLayer = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Color4Extensions.FromHex(@"221a21"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -231,7 +242,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
private void load(AudioManager audio, OsuColour colours)
|
||||
{
|
||||
flashSample = audio.Samples.Get(@"UI/default-select-disabled");
|
||||
}
|
||||
@ -256,7 +267,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
flashLayer.FadeInFromZero(80, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeOutFromOne(1500, Easing.OutQuint);
|
||||
flashSample.Play();
|
||||
flashSample?.Play();
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
@ -288,15 +299,15 @@ namespace osu.Game.Overlays.Dialog
|
||||
// Reset various animations but only if the dialog animation fully completed
|
||||
if (content.Alpha == 0)
|
||||
{
|
||||
buttonsContainer.TransformSpacingTo(buttonsEnterSpacing);
|
||||
buttonsContainer.MoveToY(buttonsEnterSpacing.Y);
|
||||
content.ScaleTo(0.7f);
|
||||
ring.ResizeTo(ringMinifiedSize);
|
||||
}
|
||||
|
||||
content.FadeIn(ENTER_DURATION, Easing.OutQuint);
|
||||
ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint);
|
||||
buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint);
|
||||
buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint);
|
||||
content
|
||||
.ScaleTo(1, 750, Easing.OutElasticHalf)
|
||||
.FadeIn(ENTER_DURATION, Easing.OutQuint);
|
||||
|
||||
ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
@ -306,7 +317,9 @@ namespace osu.Game.Overlays.Dialog
|
||||
// This is presumed to always be a sane default "cancel" action.
|
||||
buttonsContainer.Last().TriggerClick();
|
||||
|
||||
content.FadeOut(EXIT_DURATION, Easing.InSine);
|
||||
content
|
||||
.ScaleTo(0.7f, EXIT_DURATION, Easing.Out)
|
||||
.FadeOut(EXIT_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void pressButtonAtIndex(int index)
|
||||
|
@ -29,16 +29,18 @@ namespace osu.Game.Overlays
|
||||
|
||||
public DialogOverlay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Child = dialogContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
};
|
||||
|
||||
Width = 0.4f;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
Width = 500;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -143,6 +143,8 @@ namespace osu.Game.Overlays.Login
|
||||
panel.Status.BindTo(api.LocalUser.Value.Status);
|
||||
panel.Activity.BindTo(api.LocalUser.Value.Activity);
|
||||
|
||||
panel.Status.BindValueChanged(_ => updateDropdownCurrent(), true);
|
||||
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
switch (action.NewValue)
|
||||
@ -174,6 +176,24 @@ namespace osu.Game.Overlays.Login
|
||||
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form));
|
||||
});
|
||||
|
||||
private void updateDropdownCurrent()
|
||||
{
|
||||
switch (panel.Status.Value)
|
||||
{
|
||||
case UserStatus.Online:
|
||||
dropdown.Current.Value = UserAction.Online;
|
||||
break;
|
||||
|
||||
case UserStatus.DoNotDisturb:
|
||||
dropdown.Current.Value = UserAction.DoNotDisturb;
|
||||
break;
|
||||
|
||||
case UserStatus.Offline:
|
||||
dropdown.Current.Value = UserAction.AppearOffline;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
{
|
||||
[Key.Q] = new[] { typeof(ModEasy) },
|
||||
[Key.W] = new[] { typeof(ModNoFail) },
|
||||
[Key.E] = new[] { typeof(ModHalfTime) },
|
||||
[Key.E] = new[] { typeof(ModHalfTime), typeof(ModDaycore) },
|
||||
[Key.A] = new[] { typeof(ModHardRock) },
|
||||
[Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) },
|
||||
[Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) },
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -15,6 +14,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"0.00x");
|
||||
protected override LocalisableString FormatCount(double count) => ModUtils.FormatScoreMultiplier(count);
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Overlays.News
|
||||
{
|
||||
Title = PageTitleStrings.MainNewsControllerDefault;
|
||||
Description = NamedOverlayComponentStrings.NewsDescription;
|
||||
Icon = HexaconsIcons.News;
|
||||
Icon = OsuIcon.News;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, INotificationOverlay
|
||||
{
|
||||
public IconUsage Icon => HexaconsIcons.Notification;
|
||||
public IconUsage Icon => OsuIcon.Notification;
|
||||
public LocalisableString Title => NotificationsStrings.HeaderTitle;
|
||||
public LocalisableString Description => NotificationsStrings.HeaderDescription;
|
||||
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
|
||||
{
|
||||
public IconUsage Icon => HexaconsIcons.Music;
|
||||
public IconUsage Icon => OsuIcon.Music;
|
||||
public LocalisableString Title => NowPlayingStrings.HeaderTitle;
|
||||
public LocalisableString Description => NowPlayingStrings.HeaderDescription;
|
||||
|
||||
|
@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
bool anyInfoAdded = false;
|
||||
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location);
|
||||
anyInfoAdded |= tryAddInfo(OsuIcon.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation);
|
||||
|
||||
if (anyInfoAdded)
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Profile
|
||||
public ProfileHeaderTitle()
|
||||
{
|
||||
Title = PageTitleStrings.MainUsersControllerDefault;
|
||||
Icon = HexaconsIcons.Profile;
|
||||
Icon = OsuIcon.Player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Rankings
|
||||
{
|
||||
Title = PageTitleStrings.MainRankingControllerDefault;
|
||||
Description = NamedOverlayComponentStrings.RankingsDescription;
|
||||
Icon = HexaconsIcons.Rankings;
|
||||
Icon = OsuIcon.Ranking;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
{
|
||||
public partial class AudioOffsetAdjustControl : SettingsItem<double>
|
||||
{
|
||||
public IBindable<double?> SuggestedOffset => ((AudioOffsetPreview)Control).SuggestedOffset;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
LabelText = AudioSettingsStrings.AudioOffset;
|
||||
}
|
||||
|
||||
protected override Drawable CreateControl() => new AudioOffsetPreview();
|
||||
|
||||
private partial class AudioOffsetPreview : CompositeDrawable, IHasCurrentValue<double>
|
||||
{
|
||||
public Bindable<double> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>();
|
||||
|
||||
private readonly IBindableList<SessionAverageHitErrorTracker.DataPoint> averageHitErrorHistory = new BindableList<SessionAverageHitErrorTracker.DataPoint>();
|
||||
|
||||
public readonly Bindable<double?> SuggestedOffset = new Bindable<double?>();
|
||||
|
||||
private Container<Box> notchContainer = null!;
|
||||
private TextFlowContainer hintText = null!;
|
||||
private RoundedButton applySuggestion = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SessionAverageHitErrorTracker hitErrorTracker)
|
||||
{
|
||||
averageHitErrorHistory.BindTo(hitErrorTracker.AverageHitErrorHistory);
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(10),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TimeSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = { BindTarget = Current },
|
||||
KeyboardStep = 1,
|
||||
},
|
||||
notchContainer = new Container<Box>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 10,
|
||||
Padding = new MarginPadding { Horizontal = Nub.DEFAULT_EXPANDED_SIZE / 2 },
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
hintText = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 16))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
applySuggestion = new RoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Apply suggested offset",
|
||||
Action = () =>
|
||||
{
|
||||
if (SuggestedOffset.Value.HasValue)
|
||||
current.Value = SuggestedOffset.Value.Value;
|
||||
hitErrorTracker.ClearHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
averageHitErrorHistory.BindCollectionChanged(updateDisplay, true);
|
||||
SuggestedOffset.BindValueChanged(_ => updateHintText(), true);
|
||||
}
|
||||
|
||||
private void updateDisplay(object? _, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.NewItems!)
|
||||
{
|
||||
notchContainer.ForEach(n => n.Alpha *= 0.95f);
|
||||
notchContainer.Add(new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 2,
|
||||
RelativePositionAxes = Axes.X,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
X = getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset)
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (SessionAverageHitErrorTracker.DataPoint dataPoint in e.OldItems!)
|
||||
{
|
||||
var notch = notchContainer.FirstOrDefault(n => n.X == getXPositionForOffset(dataPoint.SuggestedGlobalAudioOffset));
|
||||
Debug.Assert(notch != null);
|
||||
notchContainer.Remove(notch, true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
notchContainer.Clear();
|
||||
break;
|
||||
}
|
||||
|
||||
SuggestedOffset.Value = averageHitErrorHistory.Any() ? averageHitErrorHistory.Average(dataPoint => dataPoint.SuggestedGlobalAudioOffset) : null;
|
||||
}
|
||||
|
||||
private float getXPositionForOffset(double offset) => (float)(Math.Clamp(offset, current.MinValue, current.MaxValue) / (2 * current.MaxValue));
|
||||
|
||||
private void updateHintText()
|
||||
{
|
||||
hintText.Text = SuggestedOffset.Value == null
|
||||
? @"Play a few beatmaps to receive a suggested offset!"
|
||||
: $@"Based on the last {averageHitErrorHistory.Count} play(s), the suggested offset is {SuggestedOffset.Value:N0} ms.";
|
||||
applySuggestion.Enabled.Value = SuggestedOffset.Value != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user