mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 04:02:59 +08:00
Merge branch 'master' into freemod_mapinfo_fix
This commit is contained in:
commit
644553d5b4
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.221.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -92,9 +92,10 @@ namespace osu.Desktop
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.Value == UserStatus.Online && activity.Value != null)
|
||||
if (activity.Value != null)
|
||||
{
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited;
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
||||
|
||||
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.Performance;
|
||||
using osu.Desktop.Security;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
@ -15,9 +16,11 @@ using osu.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Utils;
|
||||
using SDL2;
|
||||
|
||||
@ -28,6 +31,9 @@ namespace osu.Desktop
|
||||
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
|
||||
private ArchiveImportIPCChannel? archiveImportIPCChannel;
|
||||
|
||||
[Cached(typeof(IHighPerformanceSessionManager))]
|
||||
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
||||
|
||||
public OsuGameDesktop(string[]? args = null)
|
||||
: base(args)
|
||||
{
|
||||
@ -86,8 +92,8 @@ namespace osu.Desktop
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
{
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
|
||||
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
|
43
osu.Desktop/Performance/HighPerformanceSessionManager.cs
Normal file
43
osu.Desktop/Performance/HighPerformanceSessionManager.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// 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.Runtime;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Performance;
|
||||
|
||||
namespace osu.Desktop.Performance
|
||||
{
|
||||
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
|
||||
{
|
||||
private GCLatencyMode originalGCMode;
|
||||
|
||||
public IDisposable BeginSession()
|
||||
{
|
||||
enableHighPerformanceSession();
|
||||
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
|
||||
}
|
||||
|
||||
private void enableHighPerformanceSession()
|
||||
{
|
||||
Logger.Log("Starting high performance session");
|
||||
|
||||
originalGCMode = GCSettings.LatencyMode;
|
||||
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
||||
|
||||
// Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
|
||||
GC.Collect(0);
|
||||
}
|
||||
|
||||
private void disableHighPerformanceSession()
|
||||
{
|
||||
Logger.Log("Ending high performance session");
|
||||
|
||||
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
|
||||
GCSettings.LatencyMode = originalGCMode;
|
||||
|
||||
// No GC.Collect() as we were already collecting at a higher frequency in the old mode.
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@ -173,13 +174,16 @@ namespace osu.Desktop
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
|
17
osu.Desktop/Windows/Icons.cs
Normal file
17
osu.Desktop/Windows/Icons.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
public static class Icons
|
||||
{
|
||||
/// <summary>
|
||||
/// Fully qualified path to the directory that contains icons (in the installation folder).
|
||||
/// </summary>
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
}
|
||||
}
|
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
@ -0,0 +1,292 @@
|
||||
// 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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
|
||||
/// </summary>
|
||||
private const string default_icon = @"DefaultIcon";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the command line that the shell invokes.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
|
||||
/// </summary>
|
||||
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
|
||||
|
||||
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
|
||||
|
||||
/// <summary>
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
{
|
||||
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI association descriptions.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UninstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Uninstall();
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to uninstall file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Installs or updates associations.
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s)
|
||||
{
|
||||
if (localisation == null)
|
||||
return s.ToString();
|
||||
|
||||
var b = localisation.GetLocalisedBindableString(s);
|
||||
b.UnbindAll();
|
||||
return b.Value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
private enum EventId
|
||||
{
|
||||
/// <summary>
|
||||
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
|
||||
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
|
||||
/// </summary>
|
||||
SHCNE_ASSOCCHANGED = 0x08000000
|
||||
}
|
||||
|
||||
private enum Flags : uint
|
||||
{
|
||||
SHCNF_IDLIST = 0x0000
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||
/// </summary>
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
|
||||
/// <summary>
|
||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,4 +31,7 @@
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Windows Icons">
|
||||
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -20,6 +20,7 @@
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
<file src="**.ico" target="lib\net45\"/>
|
||||
<file src="icon.png" target=""/>
|
||||
</files>
|
||||
</package>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Fruit(), 0.01, true],
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,210 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 65316
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
DistanceSpacing: 1.4
|
||||
BeatDivisor: 4
|
||||
GridSize: 8
|
||||
TimelineZoom: 1.4
|
||||
|
||||
[Metadata]
|
||||
Title:Nanairo Symphony -TV Size-
|
||||
TitleUnicode:七色シンフォニー -TV Size-
|
||||
Artist:Coalamode.
|
||||
ArtistUnicode:コアラモード.
|
||||
Creator:Ascendance
|
||||
Version:Aru's Cup
|
||||
Source:四月は君の嘘
|
||||
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
|
||||
BeatmapID:1041052
|
||||
BeatmapSetID:488149
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:2.5
|
||||
OverallDifficulty:6
|
||||
ApproachRate:6
|
||||
SliderMultiplier:1.02
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
Video,500,"forty.avi"
|
||||
0,0,"cropped-1366-768-647733.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1155,387.096774193548,4,2,1,50,1,0
|
||||
15284,-100,4,2,1,60,0,0
|
||||
16638,-100,4,2,1,50,0,0
|
||||
41413,-100,4,2,1,60,0,0
|
||||
59993,-100,4,2,1,65,0,0
|
||||
66187,-100,4,2,1,70,0,1
|
||||
87284,-100,4,2,1,60,0,1
|
||||
87864,-100,4,2,1,70,0,0
|
||||
87961,-100,4,2,1,50,0,0
|
||||
88638,-100,4,2,1,30,0,0
|
||||
89413,-100,4,2,1,10,0,0
|
||||
89800,-100,4,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 255,128,64
|
||||
Combo2 : 0,128,255
|
||||
Combo3 : 255,128,192
|
||||
Combo4 : 0,128,192
|
||||
|
||||
[HitObjects]
|
||||
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
160,160,2122,1,0,0:0:0:0:
|
||||
272,160,2509,1,2,0:0:0:0:
|
||||
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
384,96,4058,1,2,0:0:0:0:
|
||||
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,64,5800,1,2,0:0:0:0:
|
||||
240,64,5993,1,2,0:0:0:0:
|
||||
288,64,6187,1,2,0:0:0:0:
|
||||
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
|
||||
488,160,8122,2,0,L|376:160,1,102
|
||||
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
400,288,10058,1,0,0:0:0:0:
|
||||
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
|
||||
400,288,11606,1,0,0:0:0:0:
|
||||
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
0,288,13154,1,0,0:0:0:0:
|
||||
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
|
||||
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
|
||||
312,208,15864,1,6,0:0:0:0:
|
||||
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
432,176,20896,1,0,0:0:0:0:
|
||||
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
328,176,22445,1,0,0:0:0:0:
|
||||
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
224,176,23993,1,0,0:0:0:0:
|
||||
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
416,176,25541,1,0,0:0:0:0:
|
||||
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
480,112,27090,1,0,0:0:0:0:
|
||||
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
432,112,28058,1,2,0:0:0:0:
|
||||
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
272,272,30574,2,0,L|374:272,1,102
|
||||
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
|
||||
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
200,368,32703,1,2,0:0:0:0:
|
||||
376,368,33284,1,0,0:0:0:0:
|
||||
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
280,16,36380,1,2,0:0:0:0:
|
||||
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
176,224,37928,1,2,0:0:0:0:
|
||||
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
144,264,38896,1,2,0:0:0:0:
|
||||
280,336,39477,2,0,L|336:336,1,51
|
||||
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
|
||||
352,232,40445,1,4,0:1:0:0:
|
||||
160,224,41025,1,8,0:3:0:0:
|
||||
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
400,40,41993,1,0,0:0:0:0:
|
||||
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
|
||||
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
56,136,45284,1,2,0:0:0:0:
|
||||
160,144,45671,1,8,0:0:0:0:
|
||||
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
|
||||
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
144,136,49154,1,2,0:0:0:0:
|
||||
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
|
||||
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
|
||||
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
400,208,51284,1,0,0:0:0:0:
|
||||
352,240,51477,2,0,L|240:240,1,102
|
||||
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
88,264,53025,1,2,0:0:0:0:
|
||||
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
|
||||
432,120,54574,1,2,0:0:0:0:
|
||||
328,128,54961,1,8,0:0:0:0:
|
||||
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
224,184,56509,1,8,0:3:0:0:
|
||||
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
224,272,58445,5,2,0:0:0:0:
|
||||
320,296,58832,1,0,0:0:0:0:
|
||||
224,328,59219,1,2,0:0:0:0:
|
||||
120,328,59606,1,8,0:3:0:0:
|
||||
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
160,112,64638,1,8,0:0:0:0:
|
||||
208,128,64832,1,8,0:0:0:0:
|
||||
256,144,65025,1,8,0:0:0:0:
|
||||
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
176,208,68316,1,0,0:0:0:0:
|
||||
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
|
||||
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
|
||||
208,248,77800,1,8,0:0:0:0:
|
||||
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
96,72,80703,1,0,0:0:0:0:
|
||||
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
208,272,82445,1,10,0:0:0:0:
|
||||
312,272,82832,1,8,0:0:0:0:
|
||||
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
|
||||
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
|
||||
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
|
||||
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
|
||||
168,96,87864,1,4,0:0:0:0:
|
||||
256,192,88251,12,0,89800,0:0:0:0:
|
@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
float offsetPosition = hitObject.OriginalX;
|
||||
double startTime = hitObject.StartTime;
|
||||
|
||||
if (lastPosition == null)
|
||||
if (lastPosition == null ||
|
||||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
|
||||
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
|
||||
// todo: should be revisited and corrected later probably.
|
||||
lastPosition == 0)
|
||||
{
|
||||
lastPosition = offsetPosition;
|
||||
lastStartTime = startTime;
|
||||
|
@ -2,22 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
private int fruitsHit;
|
||||
private int ticksHit;
|
||||
private int tinyTicksHit;
|
||||
private int tinyTicksMissed;
|
||||
private int misses;
|
||||
private int num300;
|
||||
private int num100;
|
||||
private int num50;
|
||||
private int numKatu;
|
||||
private int numMiss;
|
||||
|
||||
public CatchPerformanceCalculator()
|
||||
: base(new CatchRuleset())
|
||||
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
||||
|
||||
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
num300 = score.GetCount300() ?? 0; // HitResult.Great
|
||||
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
|
||||
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
|
||||
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
|
||||
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
|
||||
|
||||
// We are heavily relying on aim in catch the beat
|
||||
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||
value *= lengthBonus;
|
||||
|
||||
value *= Math.Pow(0.97, misses);
|
||||
value *= Math.Pow(0.97, numMiss);
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
|
||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
|
||||
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
|
||||
private int totalComboHits() => misses + ticksHit + fruitsHit;
|
||||
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
|
||||
private int totalSuccessfulHits() => num50 + num100 + num300;
|
||||
private int totalComboHits() => numMiss + num100 + num300;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
// matches stable.
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||
// the above early-return skips the failure check at the end of the same method:
|
||||
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||
// making it impossible to fail on a tiny droplet regardless of result.
|
||||
if (result.Type == HitResult.SmallTickMiss)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Note(), 0.01, true],
|
||||
[new HeadNote(), 0.01, true],
|
||||
[new TailNote(), 0.01, true],
|
||||
[new HoldNoteBody(), 0, true], // hold note break
|
||||
[new HoldNote(), 0, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup hierarchy", () => Child = new Container
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
Child = new Container
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@ -32,6 +33,27 @@ 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 TestPlayfieldBasedSize()
|
||||
{
|
||||
ModFlashlight mod = new OsuModFlashlight();
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = mod,
|
||||
PassCondition = () =>
|
||||
{
|
||||
var flashlightOverlay = Player.DrawableRuleset.Overlays
|
||||
.OfType<ModFlashlight<OsuHitObject>.Flashlight>()
|
||||
.First();
|
||||
|
||||
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("adjust playfield scale", () =>
|
||||
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDimsOnlyAfterStartTime()
|
||||
{
|
||||
|
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new HitCircle(), 0.01, true],
|
||||
[new SliderHeadCircle(), 0.01, true],
|
||||
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SliderTick(), 0.01, true],
|
||||
[new SliderRepeat(new Slider()), 0.01, true],
|
||||
[new SliderTailCircle(new Slider()), 0, true],
|
||||
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new Slider(), 0, true],
|
||||
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SpinnerTick(), 0, false],
|
||||
[new SpinnerBonusTick(), 0, false],
|
||||
[new Spinner(), 0.01, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
|
||||
// some spinners may not complete due to very minor decimal loss during calculation
|
||||
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,11 +80,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
foreach (var piece in DimmablePieces)
|
||||
{
|
||||
// if the specified dimmable piece is a DHO, it is generally not safe to tack transforms onto it directly
|
||||
// as they may be cleared via the `updateState()` DHO flow,
|
||||
// so use `ApplyCustomUpdateState` instead. which does not have this pitfall.
|
||||
if (piece is DrawableHitObject drawableObjectPiece)
|
||||
{
|
||||
// this method can be called multiple times, and we don't want to subscribe to the event more than once,
|
||||
// so this is what it is going to have to be...
|
||||
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
||||
}
|
||||
else
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
}
|
||||
|
||||
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
}
|
||||
|
||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override IEnumerable<Drawable> DimmablePieces => new Drawable[]
|
||||
{
|
||||
HeadCircle,
|
||||
// HeadCircle should not be added to this list, as it handles dimming itself
|
||||
TailCircle,
|
||||
repeatContainer,
|
||||
Body,
|
||||
|
@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
break;
|
||||
}
|
||||
|
||||
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
|
||||
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
|
||||
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;
|
||||
|
||||
|
@ -279,10 +279,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (HandleUserInput)
|
||||
{
|
||||
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
|
||||
bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
|
||||
|
||||
RotationTracker.Tracking = !Result.HasResult
|
||||
&& correctButtonPressed
|
||||
&& correctButtonPressed()
|
||||
&& isValidSpinningTime;
|
||||
}
|
||||
|
||||
@ -292,11 +291,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// Ticks can theoretically be judged at any point in the spinner's duration.
|
||||
// A tick must be alive to correctly play back samples,
|
||||
// but for performance reasons, we only want to keep the next tick alive.
|
||||
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged);
|
||||
DrawableHitObject nextTick = null;
|
||||
|
||||
foreach (var nested in NestedHitObjects)
|
||||
{
|
||||
if (!nested.Judged)
|
||||
{
|
||||
nextTick = nested;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
|
||||
if (next?.LifetimeStart == double.MaxValue)
|
||||
next.LifetimeStart = HitObject.StartTime;
|
||||
if (nextTick?.LifetimeStart == double.MaxValue)
|
||||
nextTick.LifetimeStart = HitObject.StartTime;
|
||||
}
|
||||
|
||||
private bool correctButtonPressed()
|
||||
{
|
||||
if (OsuActionInputManager == null)
|
||||
return false;
|
||||
|
||||
foreach (var action in OsuActionInputManager.PressedActions)
|
||||
{
|
||||
if (action == OsuAction.LeftButton || action == OsuAction.RightButton)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
|
@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
// 0.05 rad/ms, or ~477 RPM, as per stable.
|
||||
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
|
||||
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
|
||||
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000;
|
||||
|
||||
switch (h)
|
||||
{
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
@ -111,42 +110,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||
fadeCounterOnTimeStart();
|
||||
updateSpmAlpha();
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void fadeCounterOnTimeStart()
|
||||
private void updateSpmAlpha()
|
||||
{
|
||||
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSpinner.IsNotNull())
|
||||
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
|
||||
else
|
||||
spmContainer.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4.White.Opacity(0.25f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = 90 - arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
background.Alpha = spinner.Progress >= 1 ? 0 : 1;
|
||||
|
||||
fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(90 - fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(90 - fill.Progress * 180);
|
||||
}
|
||||
|
||||
private partial class ProgressFill : CircularProgress
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = -arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
base.Update();
|
||||
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(-fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(-fill.Progress * 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
@ -117,42 +116,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||
fadeCounterOnTimeStart();
|
||||
updateSpmAlpha();
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
if (!(drawableHitObject is DrawableSpinner))
|
||||
return;
|
||||
|
||||
fadeCounterOnTimeStart();
|
||||
}
|
||||
|
||||
private void fadeCounterOnTimeStart()
|
||||
private void updateSpmAlpha()
|
||||
{
|
||||
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableSpinner.IsNotNull())
|
||||
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
|
||||
else
|
||||
spmContainer.Alpha = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
SnakingOut.BindTo(configSnakingOut);
|
||||
|
||||
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
|
||||
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
if (mousePosition is Vector2 pos)
|
||||
{
|
||||
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
|
||||
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
|
||||
|
||||
// Normalise the delta to -180 .. 180
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@ -33,14 +32,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
drawableSpinner.HitObjectApplied += resetState;
|
||||
}
|
||||
|
||||
private RotationRecord lastRecord;
|
||||
|
||||
public void SetRotation(float currentRotation)
|
||||
{
|
||||
// If we've gone back in time, it's fine to work with a fresh set of records for now
|
||||
if (records.Count > 0 && Time.Current < records.Last().Time)
|
||||
if (records.Count > 0 && Time.Current < lastRecord.Time)
|
||||
records.Clear();
|
||||
|
||||
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
||||
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time))
|
||||
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, lastRecord.Time))
|
||||
return;
|
||||
|
||||
if (records.Count > 0)
|
||||
@ -52,11 +53,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||
}
|
||||
|
||||
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||
records.Enqueue(lastRecord = new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||
}
|
||||
|
||||
private void resetState(DrawableHitObject hitObject)
|
||||
{
|
||||
lastRecord = default;
|
||||
result.Value = 0;
|
||||
records.Clear();
|
||||
}
|
||||
|
@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private partial class LegacyDrawableSliderPath : DrawableSliderPath
|
||||
{
|
||||
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
|
||||
|
||||
protected new float CalculatedBorderPortion
|
||||
// Roughly matches osu!stable's slider border portions.
|
||||
=> base.CalculatedBorderPortion * 0.77f;
|
||||
|
||||
protected override Color4 ColourAt(float position)
|
||||
{
|
||||
float realBorderPortion = shadow_portion + CalculatedBorderPortion;
|
||||
float realGradientPortion = 1 - realBorderPortion;
|
||||
|
||||
if (position <= shadow_portion)
|
||||
return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion);
|
||||
|
||||
if (position <= realBorderPortion)
|
||||
return BorderColour;
|
||||
|
||||
position -= realBorderPortion;
|
||||
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L99
|
||||
// float aaWidth = Math.Min(Math.Max(0.5f / PathRadius, 3.0f / 256.0f), 1.0f / 16.0f);
|
||||
// applying the aa_width constant from stable makes sliders blurry, especially on CS>5. set to zero for now.
|
||||
// this might be related to SmoothPath applying AA internally, but disabling that does not seem to have much of an effect.
|
||||
const float aa_width = 0f;
|
||||
|
||||
Color4 shadow = new Color4(0, 0, 0, 0.25f);
|
||||
Color4 outerColour = AccentColour.Darken(0.1f);
|
||||
Color4 innerColour = lighten(AccentColour, 0.5f);
|
||||
|
||||
return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
|
||||
// https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L59-L70
|
||||
const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
|
||||
const float border_portion = 0.1875f;
|
||||
|
||||
if (position <= shadow_portion - aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, Color4.Black.Opacity(0f), shadow, 0, shadow_portion - aa_width);
|
||||
|
||||
if (position <= shadow_portion + aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, shadow, BorderColour, shadow_portion - aa_width, shadow_portion + aa_width);
|
||||
|
||||
if (position <= border_portion - aa_width)
|
||||
return BorderColour;
|
||||
|
||||
if (position <= border_portion + aa_width)
|
||||
return LegacyUtils.InterpolateNonLinear(position, BorderColour, outerColour, border_portion - aa_width, border_portion + aa_width);
|
||||
|
||||
return LegacyUtils.InterpolateNonLinear(position, outerColour, innerColour, border_portion + aa_width, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -5,7 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public enum OsuSkinConfiguration
|
||||
{
|
||||
SliderBorderSize,
|
||||
SliderPathRadius,
|
||||
CursorCentre,
|
||||
CursorExpand,
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -228,7 +227,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
|
||||
|
||||
points.Clear();
|
||||
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
|
||||
|
||||
for (int i = firstVisiblePointIndex; i < futurePointIndex; i++)
|
||||
points.Add(Source.SmokePoints[i]);
|
||||
}
|
||||
|
||||
protected sealed override void Draw(IRenderer renderer)
|
||||
|
@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissHitAndHitSwell()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[1].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, fail expected after miss
|
||||
[new Hit(), true],
|
||||
[new Hit.StrongNestedHit(new Hit()), false],
|
||||
[new DrumRollTick(new DrumRoll()), false],
|
||||
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
|
||||
[new DrumRoll(), false],
|
||||
[new SwellTick(), false],
|
||||
[new Swell(), false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
// 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;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneTaikoPlayerScroller : LegacySkinPlayerTestScene
|
||||
{
|
||||
private Storyboard? currentStoryboard;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
public void TestForegroundSpritesHidesScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller not present", () => !this.ChildrenOfType<LegacyTaikoScroller>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlaySpritesKeepsScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller present", () => this.ChildrenOfType<LegacyTaikoScroller>().Single().IsPresent);
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> base.CreateWorkingBeatmap(beatmap, currentStoryboard ?? storyboard);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinnableDrawable scroller;
|
||||
|
||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] GameplayState gameplayState)
|
||||
{
|
||||
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
|
||||
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
|
||||
.SelectMany(l => l.Elements)
|
||||
.OfType<StoryboardSprite>()
|
||||
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
|
||||
|
||||
if (spriteElements.Count() < 10)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
}
|
||||
|
||||
KeyBindingInputManager.Add(new DrumTouchInputArea());
|
||||
}
|
||||
@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
|
||||
if (scroller != null)
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
}
|
||||
|
||||
public MultiplierControlPoint ControlPointAt(double time)
|
||||
|
@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
public class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private readonly int beatmapVersion;
|
||||
|
||||
|
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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.IO;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyScoreEncoderTest
|
||||
{
|
||||
[TestCase(1, 3)]
|
||||
[TestCase(1, 0)]
|
||||
[TestCase(0, 3)]
|
||||
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
|
||||
{
|
||||
var ruleset = new CatchRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
scoreInfo.Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 50,
|
||||
[HitResult.LargeTickHit] = 5,
|
||||
[HitResult.Miss] = missCount,
|
||||
[HitResult.LargeTickMiss] = largeTickMissCount
|
||||
};
|
||||
var score = new Score { ScoreInfo = scoreInfo };
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||
encoder.Encode(encodeStream);
|
||||
|
||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||
|
||||
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
|
||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||
return decodedAfterEncode;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
@ -228,6 +230,28 @@ namespace osu.Game.Tests.Editing
|
||||
assertSnappedDistance(400, 400);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseCurrentSnap()
|
||||
{
|
||||
AddStep("add objects to beatmap", () =>
|
||||
{
|
||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||
});
|
||||
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
||||
|
||||
AddStep("seek before first object", () => EditorClock.Seek(0));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
||||
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
}
|
||||
|
||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
|
@ -538,7 +538,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
|
||||
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
|
||||
// (irrelevant in proportion to the actual filter proscribed).
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddYears(-1).AddMonths(-6)).Within(TimeSpan.FromSeconds(5)));
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddMonths(-6).AddYears(-1)).Within(TimeSpan.FromSeconds(5)));
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.EqualTo(DateTimeOffset.Now.AddMonths(-3)).Within(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
|
@ -100,6 +100,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
@ -349,8 +349,9 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private partial class FadeAccessibleResults : ResultsScreen
|
||||
{
|
||||
public FadeAccessibleResults(ScoreInfo score)
|
||||
: base(score, true)
|
||||
: base(score)
|
||||
{
|
||||
AllowRetry = true;
|
||||
}
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
|
||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override bool AllowFail => false;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
@ -130,8 +130,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
||||
{
|
||||
mainContainer.Child = new FrameStabilityContainer(gameplayStartTime)
|
||||
.WithChild(consumer = new ClockConsumingChild()));
|
||||
{
|
||||
AllowBackwardsSeeks = true,
|
||||
}.WithChild(consumer = new ClockConsumingChild());
|
||||
});
|
||||
|
||||
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -21,10 +18,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Description("player pause/fail screens")]
|
||||
public partial class TestSceneGameplayMenuOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
private FailOverlay failOverlay;
|
||||
private PauseOverlay pauseOverlay;
|
||||
private FailOverlay failOverlay = null!;
|
||||
private PauseOverlay pauseOverlay = null!;
|
||||
|
||||
private GlobalActionContainer globalActionContainer;
|
||||
private GlobalActionContainer globalActionContainer = null!;
|
||||
|
||||
private bool triggeredRetryButton;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase game)
|
||||
@ -35,12 +34,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
triggeredRetryButton = false;
|
||||
|
||||
globalActionContainer.Children = new Drawable[]
|
||||
{
|
||||
pauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = () => Logger.Log(@"Resume"),
|
||||
OnRetry = () => Logger.Log(@"Retry"),
|
||||
OnRetry = () =>
|
||||
{
|
||||
Logger.Log(@"Retry");
|
||||
triggeredRetryButton = true;
|
||||
},
|
||||
OnQuit = () => Logger.Log(@"Quit"),
|
||||
},
|
||||
failOverlay = new FailOverlay
|
||||
@ -224,17 +229,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
showOverlay();
|
||||
|
||||
bool triggered = false;
|
||||
AddStep("Click retry button", () =>
|
||||
{
|
||||
var lastAction = pauseOverlay.OnRetry;
|
||||
pauseOverlay.OnRetry = () => triggered = true;
|
||||
AddStep("Click retry button", () => getButton(1).TriggerClick());
|
||||
|
||||
getButton(1).TriggerClick();
|
||||
pauseOverlay.OnRetry = lastAction;
|
||||
});
|
||||
|
||||
AddAssert("Action was triggered", () => triggered);
|
||||
AddAssert("Retry was triggered", () => triggeredRetryButton);
|
||||
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
@ -252,25 +249,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
InputManager.Key(Key.Down);
|
||||
});
|
||||
|
||||
bool triggered = false;
|
||||
Action lastAction = null;
|
||||
AddStep("Press enter", () =>
|
||||
{
|
||||
lastAction = pauseOverlay.OnRetry;
|
||||
pauseOverlay.OnRetry = () => triggered = true;
|
||||
InputManager.Key(Key.Enter);
|
||||
});
|
||||
AddStep("Press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("Action was triggered", () =>
|
||||
{
|
||||
if (lastAction != null)
|
||||
{
|
||||
pauseOverlay.OnRetry = lastAction;
|
||||
lastAction = null;
|
||||
}
|
||||
|
||||
return triggered;
|
||||
});
|
||||
AddAssert("Retry was triggered", () => triggeredRetryButton);
|
||||
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[Test]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
|
@ -288,6 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
@ -1,26 +1,109 @@
|
||||
// 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 Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneMedalOverlay : OsuTestScene
|
||||
public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
public TestSceneMedalOverlay()
|
||||
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
private MedalOverlay overlay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep(@"display", () =>
|
||||
var overlayManagerMock = new Mock<IOverlayManager>();
|
||||
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
|
||||
|
||||
AddStep("create overlay", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
LoadComponentAsync(new MedalOverlay(new Medal
|
||||
{
|
||||
Name = @"Animations",
|
||||
InternalName = @"all-intro-doubletime",
|
||||
Description = @"More complex than you think.",
|
||||
}), Add);
|
||||
Child = overlay = new MedalOverlay(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(IOverlayManager), overlayManagerMock.Object)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicAward()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
|
||||
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleMedalsInQuickSuccession()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "S-Ranker",
|
||||
Description = "Accuracy is really underrated.",
|
||||
Slug = @"all-secret-rank-s"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "500 Combo",
|
||||
Description = "500 big ones! You're moving up in the world!",
|
||||
Slug = @"osu-combo-500"
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
|
||||
{
|
||||
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(unlock)
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private bool gameplayClockAlwaysGoingForward = true;
|
||||
private double lastForwardCheckTime;
|
||||
|
||||
public TestScenePause()
|
||||
{
|
||||
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
|
||||
@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
confirmPausedWithNoOverlay();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestForwardPlaybackGuarantee()
|
||||
{
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddUntilStep("wait for forward playback", () => Player.GameplayClockContainer.CurrentTime > 1000);
|
||||
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||
|
||||
checkForwardPlayback();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseWithLargeOffset()
|
||||
{
|
||||
double lastStopTime;
|
||||
bool alwaysGoingForward = true;
|
||||
|
||||
AddStep("force large offset", () =>
|
||||
{
|
||||
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
offset.Value = -5000;
|
||||
});
|
||||
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastStopTime = double.MinValue;
|
||||
alwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
|
||||
|
||||
double currentTime = masterClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastStopTime;
|
||||
|
||||
alwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||
|
||||
@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
resumeAndConfirm();
|
||||
|
||||
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
|
||||
checkForwardPlayback();
|
||||
|
||||
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||
}
|
||||
|
||||
private void checkForwardPlayback() => AddAssert("time didn't go too far backwards", () => gameplayClockAlwaysGoingForward);
|
||||
|
||||
private void hookForwardPlaybackCheck()
|
||||
{
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastForwardCheckTime = double.MinValue;
|
||||
gameplayClockAlwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var frameStableClock = Player.ChildrenOfType<FrameStabilityContainer>().Single().Clock;
|
||||
|
||||
double currentTime = frameStableClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastForwardCheckTime;
|
||||
lastForwardCheckTime = currentTime;
|
||||
|
||||
gameplayClockAlwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastForwardCheckTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseResume()
|
||||
{
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private TextureUpload upscale(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
@ -269,6 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||
drawableRuleset.FrameStablePlayback = true;
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
drawableRuleset.PoolSize = poolSize;
|
||||
|
||||
Child = new Container
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -12,6 +10,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -39,13 +38,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient;
|
||||
private DependenciesScreen dependenciesScreen;
|
||||
private SoloSpectatorScreen spectatorScreen;
|
||||
private DependenciesScreen dependenciesScreen = null!;
|
||||
private SoloSpectatorScreen spectatorScreen = null!;
|
||||
|
||||
private BeatmapSetInfo importedBeatmap;
|
||||
private BeatmapSetInfo importedBeatmap = null!;
|
||||
private int importedBeatmapId;
|
||||
|
||||
[SetUpSteps]
|
||||
@ -188,7 +187,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
waitForPlayerCurrent();
|
||||
|
||||
Player lastPlayer = null;
|
||||
Player lastPlayer = null!;
|
||||
AddStep("store first player", () => lastPlayer = player);
|
||||
|
||||
start();
|
||||
@ -214,6 +213,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
checkPaused(false); // Should continue playing until out of frames
|
||||
checkPaused(true); // And eventually stop after running out of frames and fail.
|
||||
// Todo: Should check for + display a failed message.
|
||||
|
||||
AddAssert("fail overlay present", () => player.ChildrenOfType<FailOverlay>().Single().IsPresent);
|
||||
AddAssert("overlay can only quit", () => player.ChildrenOfType<FailOverlay>().Single().Buttons.Single().Text == GameplayMenuOverlayStrings.Quit);
|
||||
AddStep("press quit button", () => player.ChildrenOfType<FailOverlay>().Single().Buttons.Single().TriggerClick());
|
||||
AddAssert("player exited", () => Stack.CurrentScreen is SoloSpectatorScreen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -278,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestFinalFrameInBundleHasHeader()
|
||||
{
|
||||
FrameDataBundle lastBundle = null;
|
||||
FrameDataBundle? lastBundle = null;
|
||||
|
||||
AddStep("bind to client", () => spectatorClient.OnNewFrames += (_, bundle) => lastBundle = bundle);
|
||||
|
||||
@ -287,8 +291,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
finish();
|
||||
|
||||
AddUntilStep("bundle received", () => lastBundle != null);
|
||||
AddAssert("first frame does not have header", () => lastBundle.Frames[0].Header == null);
|
||||
AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null);
|
||||
AddAssert("first frame does not have header", () => lastBundle?.Frames[0].Header == null);
|
||||
AddAssert("last frame has header", () => lastBundle?.Frames[^1].Header != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -383,7 +387,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private OsuFramedReplayInputHandler replayHandler =>
|
||||
(OsuFramedReplayInputHandler)Stack.ChildrenOfType<OsuInputManager>().First().ReplayInputHandler;
|
||||
(OsuFramedReplayInputHandler)Stack.ChildrenOfType<OsuInputManager>().First().ReplayInputHandler!;
|
||||
|
||||
private Player player => this.ChildrenOfType<Player>().Single();
|
||||
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
protected new OutroPlayer Player => (OutroPlayer)base.Player;
|
||||
|
||||
private double currentBeatmapDuration;
|
||||
|
@ -8,8 +8,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("Gain", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("Loss", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("No change", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
@ -148,7 +148,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("Was null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddStep("Became null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new SoloStatisticsUpdate(
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
|
@ -28,6 +28,21 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFastShortcutKeys()
|
||||
{
|
||||
AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
|
||||
|
||||
AddStep("press P three times", () =>
|
||||
{
|
||||
InputManager.Key(Key.P);
|
||||
InputManager.Key(Key.P);
|
||||
InputManager.Key(Key.P);
|
||||
});
|
||||
|
||||
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShortcutKeys()
|
||||
{
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowMedalAtResults()
|
||||
{
|
||||
playToResults();
|
||||
|
||||
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
})
|
||||
})
|
||||
}));
|
||||
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryFromResults()
|
||||
{
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Users;
|
||||
@ -142,7 +141,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
|
||||
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
|
||||
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo()));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo(), new OsuRuleset().RulesetInfo));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,10 +8,10 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -21,11 +21,11 @@ using osu.Game.Users;
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneSoloStatisticsWatcher : OsuTestScene
|
||||
public partial class TestSceneUserStatisticsWatcher : OsuTestScene
|
||||
{
|
||||
protected override bool UseOnlineAPI => false;
|
||||
|
||||
private SoloStatisticsWatcher watcher = null!;
|
||||
private UserStatisticsWatcher watcher = null!;
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddStep("create watcher", () =>
|
||||
{
|
||||
Child = watcher = new SoloStatisticsWatcher();
|
||||
Child = watcher = new UserStatisticsWatcher();
|
||||
});
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
// note ordering - in this test processing completes *before* the registration is added.
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
|
||||
@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
long scoreId = getScoreId();
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
long scoreId = getScoreId();
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
@ -212,7 +212,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
long scoreId = getScoreId();
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
@ -241,7 +241,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 6_000_000);
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
|
||||
@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
UserStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
@ -289,7 +289,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
||||
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<UserStatisticsUpdate> onUpdateReady) =>
|
||||
AddStep("register for updates", () =>
|
||||
{
|
||||
watcher.RegisterForStatisticsUpdateAfter(
|
@ -420,9 +420,10 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
public new LoadingSpinner RightSpinner => base.RightSpinner;
|
||||
public new ScorePanelList ScorePanelList => base.ScorePanelList;
|
||||
|
||||
public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
|
||||
: base(score, roomId, playlistItem, allowRetry)
|
||||
public TestResultsScreen([CanBeNull] ScoreInfo score, int roomId, PlaylistItem playlistItem)
|
||||
: base(score, roomId, playlistItem)
|
||||
{
|
||||
AllowRetry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
@ -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;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneDrawableRank : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestAllRanks()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer<DrawableRank>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(10),
|
||||
ChildrenEnumerable = Enum.GetValues<ScoreRank>().OrderBy(v => v).Select(rank => new DrawableRank(rank)
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
Size = new Vector2(50, 25),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics.User;
|
||||
using osu.Game.Users;
|
||||
@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
});
|
||||
|
||||
private void displayUpdate(UserStatistics before, UserStatistics after) =>
|
||||
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
|
||||
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after));
|
||||
}
|
||||
}
|
||||
|
@ -399,8 +399,9 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public TestResultsScreen(ScoreInfo score)
|
||||
: base(score, true)
|
||||
: base(score)
|
||||
{
|
||||
AllowRetry = true;
|
||||
ShowUserStatistics = true;
|
||||
}
|
||||
|
||||
@ -470,8 +471,9 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public HotkeyRetryOverlay RetryOverlay;
|
||||
|
||||
public UnrankedSoloResultsScreen(ScoreInfo score)
|
||||
: base(score, true)
|
||||
: base(score)
|
||||
{
|
||||
AllowRetry = true;
|
||||
Score!.BeatmapInfo!.OnlineID = 0;
|
||||
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -82,14 +82,14 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
|
||||
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
|
||||
{
|
||||
Child = new SoloStatisticsPanel(score)
|
||||
Child = new UserStatisticsPanel(score)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Score = { Value = score },
|
||||
StatisticsUpdate =
|
||||
DisplayedUserStatisticsUpdate =
|
||||
{
|
||||
Value = new SoloStatisticsUpdate(score, new UserStatistics
|
||||
Value = new UserStatisticsUpdate(score, new UserStatistics
|
||||
{
|
||||
Level = new UserStatistics.LevelInfo
|
||||
{
|
||||
|
@ -37,15 +37,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() => Child = advancedStats = new TestAdvancedStats
|
||||
{
|
||||
Width = 500
|
||||
Width = 500,
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("reset game ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
private BeatmapInfo exampleBeatmapInfo => new BeatmapInfo
|
||||
{
|
||||
Ruleset = rulesets.AvailableRulesets.First(),
|
||||
@ -74,45 +68,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaFirstBarTextManiaBeatmap()
|
||||
public void TestFirstBarText()
|
||||
{
|
||||
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"),
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 5,
|
||||
DrainRate = 4.3f,
|
||||
OverallDifficulty = 4.5f,
|
||||
ApproachRate = 3.1f
|
||||
},
|
||||
StarRating = 8
|
||||
});
|
||||
|
||||
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaFirstBarTextConvert()
|
||||
{
|
||||
AddStep("set game ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 5,
|
||||
DrainRate = 4.3f,
|
||||
OverallDifficulty = 4.5f,
|
||||
ApproachRate = 3.1f
|
||||
},
|
||||
StarRating = 8
|
||||
});
|
||||
|
||||
AddStep("set ruleset to mania", () => advancedStats.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
|
||||
AddStep("set ruleset to osu", () => advancedStats.Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCs);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSettingsOrder()
|
||||
{
|
||||
createScreen();
|
||||
|
||||
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
|
||||
AddAssert("mod settings order: DT, HD, DF", () =>
|
||||
{
|
||||
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
|
||||
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
|
||||
columns.ElementAt(1).Mod is OsuModHidden &&
|
||||
columns.ElementAt(2).Mod is OsuModDeflate;
|
||||
});
|
||||
|
||||
AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList());
|
||||
AddAssert("mod settings order: NC, HD, DF", () =>
|
||||
{
|
||||
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
|
||||
return columns.ElementAt(0).Mod is OsuModNightcore &&
|
||||
columns.ElementAt(1).Mod is OsuModHidden &&
|
||||
columns.ElementAt(2).Mod is OsuModDeflate;
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
|
||||
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
|
||||
|
@ -17,12 +17,15 @@ namespace osu.Game.Audio.Effects
|
||||
/// </summary>
|
||||
public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz
|
||||
|
||||
/// <summary>
|
||||
/// Whether this filter is currently attached to the audio track and thus applying an adjustment.
|
||||
/// </summary>
|
||||
public bool IsAttached { get; private set; }
|
||||
|
||||
private readonly AudioMixer mixer;
|
||||
private readonly BQFParameters filter;
|
||||
private readonly BQFType type;
|
||||
|
||||
private bool isAttached;
|
||||
|
||||
private readonly Cached filterApplication = new Cached();
|
||||
|
||||
private int cutoff;
|
||||
@ -132,22 +135,22 @@ namespace osu.Game.Audio.Effects
|
||||
|
||||
private void ensureAttached()
|
||||
{
|
||||
if (isAttached)
|
||||
if (IsAttached)
|
||||
return;
|
||||
|
||||
Debug.Assert(!mixer.Effects.Contains(filter));
|
||||
mixer.Effects.Add(filter);
|
||||
isAttached = true;
|
||||
IsAttached = true;
|
||||
}
|
||||
|
||||
private void ensureDetached()
|
||||
{
|
||||
if (!isAttached)
|
||||
if (!IsAttached)
|
||||
return;
|
||||
|
||||
Debug.Assert(mixer.Effects.Contains(filter));
|
||||
mixer.Effects.Remove(filter);
|
||||
isAttached = false;
|
||||
IsAttached = false;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -361,13 +361,20 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
const string no_videos_message = "No videos found to delete!";
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = no_videos_message });
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
Text = $"Preparing to delete all {HumanisedModelName} videos...",
|
||||
CompletionText = "No videos found to delete!",
|
||||
CompletionText = no_videos_message,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
Size size = image.Size();
|
||||
Size size = image.Size;
|
||||
|
||||
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
|
||||
// Also assume that all backgrounds are wider than they are tall, so the
|
||||
|
@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
Dimmed.BindValueChanged(_ => updateState());
|
||||
|
||||
playButton.Playing.BindValueChanged(_ => updateState(), true);
|
||||
((IBindable<double>)progress.Current).BindTo(playButton.Progress);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
progress.Progress = playButton.Progress.Value;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Storyboards;
|
||||
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
float startValue = Parsing.ParseFloat(split[4]);
|
||||
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
private readonly DecouplingFramedClock decoupledTrack;
|
||||
private readonly InterpolatingFramedClock interpolatedTrack;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
|
||||
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
|
||||
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
|
||||
if (applyOffsets)
|
||||
{
|
||||
@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
}
|
||||
|
||||
public string GetSnapshot()
|
||||
{
|
||||
return
|
||||
$"originalSource: {output(Source)}\n" +
|
||||
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
|
||||
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
|
||||
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
|
||||
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
|
||||
$"decoupledTrack: {output(decoupledTrack)}\n" +
|
||||
$"finalClockSource: {output(finalClockSource)}\n";
|
||||
|
||||
string output(IClock? clock)
|
||||
{
|
||||
if (clock == null)
|
||||
return "null";
|
||||
|
||||
if (clock is IFrameBasedClock framed)
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
|
||||
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,6 +115,11 @@ namespace osu.Game.Collections
|
||||
};
|
||||
}
|
||||
|
||||
public override bool IsPresent => base.IsPresent
|
||||
// Safety for low pass filter potentially getting stuck in applied state due to
|
||||
// transforms on `this` causing children to no longer be updated.
|
||||
|| lowPassFilter.IsAttached;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
||||
|
@ -382,7 +382,7 @@ namespace osu.Game.Database
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||
r.All<ScoreInfo>()
|
||||
.Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.Where(s => s.TotalScoreVersion < 30000013) // last total score version with a significant change to ranks
|
||||
.AsEnumerable()
|
||||
// must be done after materialisation, as realm doesn't support
|
||||
// filtering on nested property predicates or projection via `.Select()`
|
||||
|
@ -105,7 +105,12 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public void Delete(List<TModel> items, bool silent = false)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
if (items.Count == 0)
|
||||
{
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = $"No {HumanisedModelName}s found to delete!" });
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
@ -142,7 +147,12 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public void Undelete(List<TModel> items, bool silent = false)
|
||||
{
|
||||
if (!items.Any()) return;
|
||||
if (!items.Any())
|
||||
{
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = $"No {HumanisedModelName}s found to restore!" });
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
|
@ -365,6 +365,17 @@ namespace osu.Game.Database
|
||||
+ bonusProportion) * modMultiplier);
|
||||
}
|
||||
|
||||
// see similar check above.
|
||||
// if there is no legacy combo score, all combo conversion operations below
|
||||
// are either pointless or wildly wrong.
|
||||
if (maximumLegacyComboScore + maximumLegacyBonusScore == 0)
|
||||
{
|
||||
return (long)Math.Round((
|
||||
500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
}
|
||||
|
||||
// Assumptions:
|
||||
// - sliders and slider ticks are uniformly distributed in the beatmap, and thus can be ignored without losing much precision.
|
||||
// We thus consider a map of hit-circles only, which gives objectCount == maximumCombo.
|
||||
@ -404,7 +415,7 @@ namespace osu.Game.Database
|
||||
|
||||
// Calculate how many times the longest combo the user has achieved in the play can repeat
|
||||
// without exceeding the combo portion in score V1 as achieved by the player.
|
||||
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
|
||||
// This intentionally does not operate on object count and uses only score instead.
|
||||
double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
|
||||
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
|
||||
|
||||
@ -415,13 +426,12 @@ namespace osu.Game.Database
|
||||
// ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score.
|
||||
double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore
|
||||
double scoreBasedEstimateOfComboPortionInStandardisedScore
|
||||
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
|
||||
+ remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Compute approximate upper estimate new score for that play.
|
||||
// This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths.
|
||||
// There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice.
|
||||
remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
|
||||
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
|
||||
@ -438,7 +448,17 @@ namespace osu.Game.Database
|
||||
// we can skip adding the 1 and just multiply by x ^ 0.5.
|
||||
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
double objectCountBasedEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Enforce some invariants on both of the estimates.
|
||||
// In rare cases they can produce invalid results.
|
||||
scoreBasedEstimateOfComboPortionInStandardisedScore =
|
||||
Math.Clamp(scoreBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
|
||||
objectCountBasedEstimateOfComboPortionInStandardisedScore =
|
||||
Math.Clamp(objectCountBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore = Math.Min(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
|
||||
double upperEstimateOfComboPortionInStandardisedScore = Math.Max(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
|
||||
|
||||
// Approximate by combining lower and upper estimates.
|
||||
// As the lower-estimate is very pessimistic, we use a 30/70 ratio
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private Sample samplePopIn;
|
||||
private Sample samplePopOut;
|
||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
protected virtual string? PopInSampleName => @"UI/overlay-pop-in";
|
||||
protected virtual string? PopOutSampleName => @"UI/overlay-pop-out";
|
||||
protected virtual double PopInOutSampleBalance => 0;
|
||||
|
||||
protected override bool BlockNonPositionalInput => true;
|
||||
@ -34,19 +32,23 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
protected virtual bool DimMainContent => true;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IOverlayManager overlayManager { get; set; }
|
||||
[Resolved]
|
||||
private IOverlayManager? overlayManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; }
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
private Sample? samplePopIn;
|
||||
private Sample? samplePopOut;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager? audio)
|
||||
{
|
||||
samplePopIn = audio.Samples.Get(PopInSampleName);
|
||||
samplePopOut = audio.Samples.Get(PopOutSampleName);
|
||||
if (!string.IsNullOrEmpty(PopInSampleName))
|
||||
samplePopIn = audio?.Samples.Get(PopInSampleName);
|
||||
|
||||
if (!string.IsNullOrEmpty(PopOutSampleName))
|
||||
samplePopOut = audio?.Samples.Get(PopOutSampleName);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -127,7 +127,7 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
|
||||
protected virtual void ScrollFromMouseEvent(MouseEvent e) =>
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim]) * Content.DrawSize[ScrollDim], true, DistanceDecayOnRightMouseScrollbar);
|
||||
ScrollTo(Clamp(ToLocalSpace(e.ScreenSpaceMousePosition)[ScrollDim] / DrawSize[ScrollDim] * Content.DrawSize[ScrollDim]), true, DistanceDecayOnRightMouseScrollbar);
|
||||
|
||||
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction);
|
||||
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
if (dragRotationState == DragRotationState.Rotating && distance > 0)
|
||||
{
|
||||
Vector2 offset = e.MousePosition - positionMouseDown;
|
||||
float degrees = MathUtils.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
|
||||
float degrees = float.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
|
||||
|
||||
// Always rotate in the direction of least distance
|
||||
float diff = (degrees - activeCursor.Rotation) % 360;
|
||||
@ -220,12 +220,16 @@ namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
activeCursor.FadeTo(1, 250, Easing.OutQuint);
|
||||
activeCursor.ScaleTo(1, 400, Easing.OutQuint);
|
||||
activeCursor.RotateTo(0, 400, Easing.OutQuint);
|
||||
dragRotationState = DragRotationState.NotDragging;
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
activeCursor.FadeTo(0, 250, Easing.OutQuint);
|
||||
activeCursor.ScaleTo(0.6f, 250, Easing.In);
|
||||
activeCursor.RotateTo(0, 400, Easing.OutQuint);
|
||||
dragRotationState = DragRotationState.NotDragging;
|
||||
}
|
||||
|
||||
private void playTapSample(double baseFrequency = 1f)
|
||||
|
@ -63,8 +63,12 @@ namespace osu.Game.Graphics
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"ff8e5d");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"ff5a5a");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"3f3f3f");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Extensions;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class OsuNumberBox : OsuTextBox
|
||||
{
|
||||
protected override bool AllowIme => false;
|
||||
|
||||
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
|
||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
using (Stream s = entry.OpenEntryStream())
|
||||
s.ReadToFill(owner.Memory.Span);
|
||||
s.ReadExactly(owner.Memory.Span);
|
||||
|
||||
return new MemoryOwnerMemoryStream(owner);
|
||||
}
|
||||
|
@ -104,16 +104,31 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!");
|
||||
|
||||
/// <summary>
|
||||
/// "No collections found to delete!"
|
||||
/// </summary>
|
||||
public static LocalisableString NoCollectionsFoundToDelete => new TranslatableString(getKey(@"no_collections_found_to_delete"), @"No collections found to delete!");
|
||||
|
||||
/// <summary>
|
||||
/// "Deleted all mod presets!"
|
||||
/// </summary>
|
||||
public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!");
|
||||
|
||||
/// <summary>
|
||||
/// "No mod presets found to delete!"
|
||||
/// </summary>
|
||||
public static LocalisableString NoModPresetsFoundToDelete => new TranslatableString(getKey(@"no_mod_presets_found_to_delete"), @"No mod presets found to delete!");
|
||||
|
||||
/// <summary>
|
||||
/// "Restored all deleted mod presets!"
|
||||
/// </summary>
|
||||
public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!");
|
||||
|
||||
/// <summary>
|
||||
/// "No mod presets found to restore!"
|
||||
/// </summary>
|
||||
public static LocalisableString NoModPresetsFoundToRestore => new TranslatableString(getKey(@"no_mod_presets_found_to_restore"), @"No mod presets found to restore!");
|
||||
|
||||
/// <summary>
|
||||
/// "Please select your osu!stable install location"
|
||||
/// </summary>
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Performance points will not be granted due to active mods."
|
||||
/// </summary>
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
@ -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 osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class WindowsAssociationManagerStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.WindowsAssociationManager";
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuBeatmap => new TranslatableString(getKey(@"osu_beatmap"), @"osu! Beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Replay"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuReplay => new TranslatableString(getKey(@"osu_replay"), @"osu! Replay");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Skin"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuSkin => new TranslatableString(getKey(@"osu_skin"), @"osu! Skin");
|
||||
|
||||
/// <summary>
|
||||
/// "osu!"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuProtocol => new TranslatableString(getKey(@"osu_protocol"), @"osu!");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Multiplayer"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuMultiplayer => new TranslatableString(getKey(@"osu_multiplayer"), @"osu! Multiplayer");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
RulesetID = score.RulesetID,
|
||||
Passed = score.Passed,
|
||||
Mods = score.APIMods,
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Online.Notifications.WebSocket.Requests;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
|
@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"473625");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"512525");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"CC3333");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message sent from the server when new messages arrive.
|
@ -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;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php
|
||||
/// </summary>
|
||||
public class NewPrivateNotificationEvent
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public ulong ID { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("object_type")]
|
||||
public string ObjectType { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("object_id")]
|
||||
public ulong ObjectId { get; set; }
|
||||
|
||||
[JsonProperty("source_user_id")]
|
||||
public uint SourceUserID { get; set; }
|
||||
|
||||
[JsonProperty("is_read")]
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
[JsonProperty("details")]
|
||||
public JObject? Details { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php
|
||||
/// </summary>
|
||||
public class UserAchievementUnlock
|
||||
{
|
||||
[JsonProperty("achievement_id")]
|
||||
public uint AchievementId { get; set; }
|
||||
|
||||
[JsonProperty("achievement_mode")]
|
||||
public ushort? AchievementMode { get; set; }
|
||||
|
||||
[JsonProperty("cover_url")]
|
||||
public string CoverUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
public uint UserId { get; set; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client no longer wants to receive chat messages.
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client wants to receive chat messages.
|
@ -4,12 +4,12 @@
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.Solo
|
||||
namespace osu.Game.Online
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains data about the change in a user's profile statistics after completing a score.
|
||||
/// </summary>
|
||||
public class SoloStatisticsUpdate
|
||||
public class UserStatisticsUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// The score set by the user that triggered the update.
|
||||
@ -27,12 +27,12 @@ namespace osu.Game.Online.Solo
|
||||
public UserStatistics After { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SoloStatisticsUpdate"/>.
|
||||
/// Creates a new <see cref="UserStatisticsUpdate"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The score set by the user that triggered the update.</param>
|
||||
/// <param name="before">The user's profile statistics prior to the score being set.</param>
|
||||
/// <param name="after">The user's profile statistics after the score was set.</param>
|
||||
public SoloStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
|
||||
public UserStatisticsUpdate(ScoreInfo score, UserStatistics before, UserStatistics after)
|
||||
{
|
||||
Score = score;
|
||||
Before = before;
|
@ -15,15 +15,15 @@ using osu.Game.Online.Spectator;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.Solo
|
||||
namespace osu.Game.Online
|
||||
{
|
||||
/// <summary>
|
||||
/// A persistent component that binds to the spectator server and API in order to deliver updates about the logged in user's gameplay statistics.
|
||||
/// </summary>
|
||||
public partial class SoloStatisticsWatcher : Component
|
||||
public partial class UserStatisticsWatcher : Component
|
||||
{
|
||||
public IBindable<SoloStatisticsUpdate?> LatestUpdate => latestUpdate;
|
||||
private readonly Bindable<SoloStatisticsUpdate?> latestUpdate = new Bindable<SoloStatisticsUpdate?>();
|
||||
public IBindable<UserStatisticsUpdate?> LatestUpdate => latestUpdate;
|
||||
private readonly Bindable<UserStatisticsUpdate?> latestUpdate = new Bindable<UserStatisticsUpdate?>();
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
@ -120,7 +120,7 @@ namespace osu.Game.Online.Solo
|
||||
latestStatistics.TryGetValue(rulesetName, out UserStatistics? latestRulesetStatistics);
|
||||
latestRulesetStatistics ??= new UserStatistics();
|
||||
|
||||
latestUpdate.Value = new SoloStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
|
||||
latestUpdate.Value = new UserStatisticsUpdate(scoreInfo, latestRulesetStatistics, updatedStatistics);
|
||||
latestStatistics[rulesetName] = updatedStatistics;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user