1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 03:22:57 +08:00

Merge branch 'master' into discord-rpc-invites

This commit is contained in:
jvyden 2024-03-19 15:29:52 -04:00
commit f851716a47
No known key found for this signature in database
GPG Key ID: 18BCF2BE0262B278
181 changed files with 3311 additions and 1129 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -92,8 +92,8 @@ namespace osu.Desktop
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry() private string? getStableInstallPathFromRegistry()
{ {
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu")) using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
} }
protected override UpdateManager CreateUpdateManager() protected override UpdateManager CreateUpdateManager()

View File

@ -5,6 +5,7 @@ using System;
using System.IO; using System.IO;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using osu.Desktop.LegacyIpc; using osu.Desktop.LegacyIpc;
using osu.Desktop.Windows;
using osu.Framework; using osu.Framework;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -173,13 +174,16 @@ namespace osu.Desktop
{ {
tools.CreateShortcutForThisExe(); tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.InstallAssociations();
}, onAppUpdate: (_, tools) => }, onAppUpdate: (_, tools) =>
{ {
tools.CreateUninstallerRegistryEntry(); tools.CreateUninstallerRegistryEntry();
WindowsAssociationManager.UpdateAssociations();
}, onAppUninstall: (_, tools) => }, onAppUninstall: (_, tools) =>
{ {
tools.RemoveShortcutForThisExe(); tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry(); tools.RemoveUninstallerRegistryEntry();
WindowsAssociationManager.UninstallAssociations();
}, onEveryRun: (_, _, _) => }, onEveryRun: (_, _, _) =>
{ {
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently

View 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");
}
}

View 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);
}
}
}
}

View File

@ -31,4 +31,7 @@
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Windows Icons">
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project> </Project>

View File

@ -20,6 +20,7 @@
<file src="**.dll" target="lib\net45\"/> <file src="**.dll" target="lib\net45\"/>
<file src="**.config" target="lib\net45\"/> <file src="**.config" target="lib\net45\"/>
<file src="**.json" target="lib\net45\"/> <file src="**.json" target="lib\net45\"/>
<file src="**.ico" target="lib\net45\"/>
<file src="icon.png" target=""/> <file src="icon.png" target=""/>
</files> </files>
</package> </package>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="nunit" Version="3.14.0" /> <PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup> </ItemGroup>

View File

@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })] [TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
[TestCase("112643")] [TestCase("112643")]
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
public new void Test(string name, params Type[] mods) => base.Test(name, mods); public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View 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

View File

@ -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:

View File

@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
float offsetPosition = hitObject.OriginalX; float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime; 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; lastPosition = offsetPosition;
lastStartTime = startTime; lastStartTime = startTime;

View File

@ -2,22 +2,21 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Rulesets.Catch.Difficulty namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchPerformanceCalculator : PerformanceCalculator public class CatchPerformanceCalculator : PerformanceCalculator
{ {
private int fruitsHit; private int num300;
private int ticksHit; private int num100;
private int tinyTicksHit; private int num50;
private int tinyTicksMissed; private int numKatu;
private int misses; private int numMiss;
public CatchPerformanceCalculator() public CatchPerformanceCalculator()
: base(new CatchRuleset()) : base(new CatchRuleset())
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
var catchAttributes = (CatchDifficultyAttributes)attributes; var catchAttributes = (CatchDifficultyAttributes)attributes;
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great); num300 = score.GetCount300() ?? 0; // HitResult.Great
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
misses = score.Statistics.GetValueOrDefault(HitResult.Miss); numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
// We are heavily relying on aim in catch the beat // 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; 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); (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
value *= lengthBonus; value *= lengthBonus;
value *= Math.Pow(0.97, misses); value *= Math.Pow(0.97, numMiss);
// Combo scaling // Combo scaling
if (catchAttributes.MaxCombo > 0) 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 double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; private int totalSuccessfulHits() => num50 + num100 + num300;
private int totalComboHits() => misses + ticksHit + fruitsHit; private int totalComboHits() => numMiss + num100 + num300;
} }
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; 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 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) protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{ {
double increase = 0; double increase = 0;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring; 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. // No matter what, mania doesn't have passive HP drain.
Assert.That(processor.DrainRate, Is.Zero); 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);
}
} }
} }

View File

@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("setup hierarchy", () => Child = new Container AddStep("setup hierarchy", () =>
{ {
Clock = new FramedClock(clock = new ManualClock()), Child = new Container
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{ {
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", () => AddStep("retrieve config bindable", () =>
{ {

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight; return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= onSkinChanged;
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {
// Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
upSprite = new Sprite upSprite = new Sprite
{ {
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Texture = skin.GetTexture(upImage), Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 1 Width = 1
}, },
downSprite = new Sprite downSprite = new Sprite
{ {
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Texture = skin.GetTexture(downImage), Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 1, Width = 1,
Alpha = 0 Alpha = 0

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => PassCondition = () =>
{ {
var flashlightOverlay = Player.DrawableRuleset.Overlays var flashlightOverlay = Player.DrawableRuleset.Overlays
.OfType<ModFlashlight<OsuHitObject>.Flashlight>() .ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First(); .First();
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize()); return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());

View 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);
}
}
}

View File

@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Tests
var skinnable = firstObject.ApproachCircle; var skinnable = firstObject.ApproachCircle;
if (skin == null && skinnable?.Drawable is DefaultApproachCircle) if (skin == null && skinnable.Drawable is DefaultApproachCircle)
// check for default skin provider // check for default skin provider
return true; return true;
var text = skinnable?.Drawable as SpriteText; var text = skinnable.Drawable as SpriteText;
return text?.Text == skin; return text?.Text == skin;
}); });

View File

@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests
assertMidSliderJudgementFail(); assertMidSliderJudgementFail();
} }
[Test]
public void TestRewindHandling()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 },
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
}, new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(250, 0),
}, 250),
});
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4));
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
}
private void assertAllMaxJudgements() private void assertAllMaxJudgements()
{ {
AddAssert("All judgements max", () => AddAssert("All judgements max", () =>

View File

@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex) private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
{ {
if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}."); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0."); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0."); ArgumentOutOfRangeException.ThrowIfNegative(endIndex);
int extendedEndIndex = endIndex; int extendedEndIndex = endIndex;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private partial class OsuEditorPlayfield : OsuPlayfield private partial class OsuEditorPlayfield : OsuPlayfield
{ {
protected override GameplayCursorContainer CreateCursor() => null; protected override GameplayCursorContainer? CreateCursor() => null;
public OsuEditorPlayfield() public OsuEditorPlayfield()
{ {

View File

@ -334,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns> /// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1) private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
{ {
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1; int sourceIndex = -1;

View File

@ -0,0 +1,20 @@
// 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuSliderJudgementResult : OsuJudgementResult
{
public readonly Stack<(double time, bool tracking)> TrackingHistory = new Stack<(double, bool)>();
public OsuSliderJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
TrackingHistory.Push((double.NegativeInfinity, false));
}
}
}

View File

@ -95,12 +95,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// </summary> /// </summary>
private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider) private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
{ {
var oldHitAction = slider.HitArea.Hit; slider.HitArea.CanBeHit = () => !slider.DrawableSlider.AllJudged;
slider.HitArea.Hit = () =>
{
oldHitAction?.Invoke();
return !slider.DrawableSlider.AllJudged;
};
} }
private void applyEarlyFading(DrawableHitCircle circle) private void applyEarlyFading(DrawableHitCircle circle)

View File

@ -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, // 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 // some spinners may not complete due to very minor decimal loss during calculation
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration); 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));
} }
} }
} }

View File

@ -1,16 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -28,35 +24,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle public partial class DrawableHitCircle : DrawableOsuHitObject, IHasApproachCircle
{ {
public OsuAction? HitAction => HitArea?.HitAction; public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public SkinnableDrawable ApproachCircle { get; private set; } public SkinnableDrawable ApproachCircle { get; private set; } = null!;
public HitReceptor HitArea { get; private set; } public HitReceptor HitArea { get; private set; } = null!;
public SkinnableDrawable CirclePiece { get; private set; } public SkinnableDrawable CirclePiece { get; private set; } = null!;
protected override IEnumerable<Drawable> DimmablePieces => new[] protected override IEnumerable<Drawable> DimmablePieces => new[] { CirclePiece };
{
CirclePiece,
};
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle; Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
private Container scaleContainer; private Container scaleContainer = null!;
private InputManager inputManager; private ShakeContainer shakeContainer = null!;
public DrawableHitCircle() public DrawableHitCircle()
: this(null) : this(null)
{ {
} }
public DrawableHitCircle([CanBeNull] HitCircle h = null) public DrawableHitCircle(HitCircle? h = null)
: base(h) : base(h)
{ {
} }
private ShakeContainer shakeContainer;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -73,14 +64,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
HitArea = new HitReceptor HitArea = new HitReceptor
{ {
Hit = () => CanBeHit = () => !AllJudged,
{ Hit = () => UpdateResult(true)
if (AllJudged)
return false;
UpdateResult(true);
return true;
},
}, },
shakeContainer = new ShakeContainer shakeContainer = new ShakeContainer
{ {
@ -114,13 +99,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
} }
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override double LifetimeStart public override double LifetimeStart
{ {
get => base.LifetimeStart; get => base.LifetimeStart;
@ -155,7 +133,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyMinResult(); {
ApplyResult((r, position) =>
{
var circleResult = (OsuHitCircleJudgementResult)r;
circleResult.Type = r.Judgement.MinResult;
circleResult.CursorPositionAtHit = position;
}, computeHitPosition());
}
return; return;
} }
@ -169,22 +155,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || clickAction != ClickAction.Hit) if (result == HitResult.None || clickAction != ClickAction.Hit)
return; return;
Vector2? hitPosition = null;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
if (result.IsHit())
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
hitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
}
ApplyResult<(HitResult result, Vector2? position)>((r, state) => ApplyResult<(HitResult result, Vector2? position)>((r, state) =>
{ {
var circleResult = (OsuHitCircleJudgementResult)r; var circleResult = (OsuHitCircleJudgementResult)r;
circleResult.Type = state.result; circleResult.Type = state.result;
circleResult.CursorPositionAtHit = state.position; circleResult.CursorPositionAtHit = state.position;
}, (result, hitPosition)); }, (result, computeHitPosition()));
}
private Vector2? computeHitPosition()
{
if (HitArea.ClosestPressPosition is Vector2 screenSpaceHitPosition)
return HitObject.StackedPosition + (ToLocalSpace(screenSpaceHitPosition) - DrawSize / 2);
return null;
} }
/// <summary> /// <summary>
@ -227,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break; break;
case ArmedState.Idle: case ArmedState.Idle:
HitArea.HitAction = null; HitArea.Reset();
break; break;
case ArmedState.Miss: case ArmedState.Miss:
@ -247,9 +232,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// IsHovered is used // IsHovered is used
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
public Func<bool> Hit; /// <summary>
/// Whether the hitobject can still be hit at the current point in time.
/// </summary>
public required Func<bool> CanBeHit { get; set; }
public OsuAction? HitAction; /// <summary>
/// An action that's invoked to perform the hit.
/// </summary>
public required Action Hit { get; set; }
/// <summary>
/// The <see cref="OsuAction"/> with which the hit was attempted.
/// </summary>
public OsuAction? HitAction { get; private set; }
/// <summary>
/// The closest position to the hit receptor at the point where the hit was attempted.
/// </summary>
public Vector2? ClosestPressPosition { get; private set; }
public HitReceptor() public HitReceptor()
{ {
@ -264,12 +265,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool OnPressed(KeyBindingPressEvent<OsuAction> e) public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{ {
if (!CanBeHit())
return false;
switch (e.Action) switch (e.Action)
{ {
case OsuAction.LeftButton: case OsuAction.LeftButton:
case OsuAction.RightButton: case OsuAction.RightButton:
if (IsHovered && (Hit?.Invoke() ?? false)) if (ClosestPressPosition is Vector2 curClosest)
{ {
float oldDist = Vector2.DistanceSquared(curClosest, ScreenSpaceDrawQuad.Centre);
float newDist = Vector2.DistanceSquared(e.ScreenSpaceMousePosition, ScreenSpaceDrawQuad.Centre);
if (newDist < oldDist)
ClosestPressPosition = e.ScreenSpaceMousePosition;
}
else
ClosestPressPosition = e.ScreenSpaceMousePosition;
if (IsHovered)
{
Hit();
HitAction ??= e.Action; HitAction ??= e.Action;
return true; return true;
} }
@ -283,13 +299,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e) public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{ {
} }
/// <summary>
/// Resets to a fresh state.
/// </summary>
public void Reset()
{
HitAction = null;
ClosestPressPosition = null;
}
} }
private partial class ProxyableSkinnableDrawable : SkinnableDrawable private partial class ProxyableSkinnableDrawable : SkinnableDrawable
{ {
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable> defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling) public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(lookup, defaultImplementation, confineMode) : base(lookup, defaultImplementation, confineMode)
{ {
} }

View File

@ -14,8 +14,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public new Slider HitObject => (Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;
public new OsuSliderJudgementResult Result => (OsuSliderJudgementResult)base.Result;
public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child;
@ -134,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}, true); }, true);
} }
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement);
protected override void OnApply() protected override void OnApply()
{ {
base.OnApply(); base.OnApply();

View File

@ -79,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.OnApply(); base.OnApply();
Position = HitObject.Position - DrawableSlider.Position; Position = HitObject.Position - DrawableSlider.Position;
hasRotation = false;
} }
protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset); protected override void CheckForResult(bool userTriggered, double timeOffset) => DrawableSlider.SliderInputManager.TryJudgeNestedObject(this, timeOffset);
@ -146,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break; 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) while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360; aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;

View File

@ -5,11 +5,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Play;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary> /// </summary>
public bool Tracking { get; private set; } public bool Tracking { get; private set; }
[Resolved]
private IGameplayClock? gameplayClock { get; set; }
/// <summary> /// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle. /// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
/// ///
@ -49,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderInputManager(DrawableSlider slider) public SliderInputManager(DrawableSlider slider)
{ {
this.slider = slider; this.slider = slider;
this.slider.HitObjectApplied += resetState;
} }
/// <summary> /// <summary>
@ -208,6 +215,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param> /// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
private void updateTracking(bool isValidTrackingPosition) private void updateTracking(bool isValidTrackingPosition)
{ {
if (gameplayClock?.IsRewinding == true)
{
var trackingHistory = slider.Result.TrackingHistory;
while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time)
trackingHistory.Pop();
Debug.Assert(trackingHistory.Count > 0);
Tracking = trackingHistory.Peek().tracking;
return;
}
bool wasTracking = Tracking;
// from the point at which the head circle is hit, this will be non-null. // from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed. // it may be null if the head circle was missed.
OsuAction? headCircleHitAction = getInitialHitAction(); OsuAction? headCircleHitAction = getInitialHitAction();
@ -247,6 +268,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
&& isValidTrackingPosition && isValidTrackingPosition
// valid action // valid action
&& validTrackingAction; && validTrackingAction;
if (wasTracking != Tracking)
slider.Result.TrackingHistory.Push((Time.Current, Tracking));
} }
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction; private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
@ -264,5 +288,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return action == OsuAction.LeftButton || action == OsuAction.RightButton; return action == OsuAction.LeftButton || action == OsuAction.RightButton;
} }
private void resetState(DrawableHitObject obj)
{
Tracking = false;
timeToAcceptAnyKeyAfter = null;
lastPressedActions.Clear();
screenSpaceMousePosition = null;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
slider.HitObjectApplied -= resetState;
}
} }
} }

View File

@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// 0.05 rad/ms, or ~477 RPM, as per stable. // 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. // 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; 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) switch (h)
{ {

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Origin = Anchor.Centre, Origin = Anchor.Centre,
Colour = Color4.White.Opacity(0.25f), Colour = Color4.White.Opacity(0.25f),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Current = { Value = arc_fill }, Progress = arc_fill,
Rotation = 90 - arc_fill * 180, Rotation = 90 - arc_fill * 180,
InnerRadius = arc_radius, InnerRadius = arc_radius,
RoundedCaps = true, RoundedCaps = true,
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
background.Alpha = spinner.Progress >= 1 ? 0 : 1; 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.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 private partial class ProgressFill : CircularProgress

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Current = { Value = arc_fill }, Progress = arc_fill,
Rotation = -arc_fill * 180, Rotation = -arc_fill * 180,
InnerRadius = arc_radius, InnerRadius = arc_radius,
RoundedCaps = true, RoundedCaps = true,
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
base.Update(); 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.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);
} }
} }
} }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (mousePosition is Vector2 pos) 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; float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
// Normalise the delta to -180 .. 180 // Normalise the delta to -180 .. 180

View File

@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -191,16 +192,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
for (int c = 0; c < points_per_dimension; c++) for (int c = 0; c < points_per_dimension; c++)
{ {
HitPointType pointType = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius bool isHit = Vector2.Distance(new Vector2(c + 0.5f, r + 0.5f), centre) <= innerRadius;
? HitPointType.Hit
: HitPointType.Miss;
var point = new HitPoint(pointType, this) if (isHit)
{ {
BaseColour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) points[r][c] = new HitPoint(this)
}; {
BaseColour = new Color4(102, 255, 204, 255)
points[r][c] = point; };
}
else
{
points[r][c] = new MissPoint
{
BaseColour = new Color4(255, 102, 102, 255)
};
}
} }
} }
@ -246,44 +253,35 @@ 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. // 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. // 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)); var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. float localRadius = localCentre.X * inner_portion * normalisedDistance;
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
// Find the most relevant hit point. // Find the most relevant hit point.
int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); int r = (int)Math.Round(localPoint.Y);
int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); int c = (int)Math.Round(localPoint.X);
PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw(); bufferedGrid.ForceRedraw();
} }
private partial class HitPoint : Circle private abstract partial class GridPoint : CompositeDrawable
{ {
/// <summary> /// <summary>
/// The base colour which will be lightened/darkened depending on the value of this <see cref="HitPoint"/>. /// The base colour which will be lightened/darkened depending on the value of this <see cref="HitPoint"/>.
/// </summary> /// </summary>
public Color4 BaseColour; public Color4 BaseColour;
private readonly HitPointType pointType; public override bool IsPresent => Count > 0;
private readonly AccuracyHeatmap heatmap;
public override bool IsPresent => count > 0; protected int Count { get; private set; }
public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
{
this.pointType = pointType;
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
Alpha = 1;
}
private int count;
/// <summary> /// <summary>
/// Increment the value of this point by one. /// Increment the value of this point by one.
@ -291,7 +289,41 @@ namespace osu.Game.Rulesets.Osu.Statistics
/// <returns>The value after incrementing.</returns> /// <returns>The value after incrementing.</returns>
public int Increment() public int Increment()
{ {
return ++count; return ++Count;
}
}
private partial class MissPoint : GridPoint
{
public MissPoint()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Icon = FontAwesome.Solid.Times
};
}
protected override void Update()
{
Alpha = 0.8f;
Colour = BaseColour;
}
}
private partial class HitPoint : GridPoint
{
private readonly AccuracyHeatmap heatmap;
public HitPoint(AccuracyHeatmap heatmap)
{
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
InternalChild = new Circle { RelativeSizeAxes = Axes.Both };
} }
protected override void Update() protected override void Update()
@ -307,10 +339,10 @@ namespace osu.Game.Rulesets.Osu.Statistics
float amount = 0; float amount = 0;
// give some amount of alpha regardless of relative count // give some amount of alpha regardless of relative count
amount += non_relative_portion * Math.Min(1, count / 10f); amount += non_relative_portion * Math.Min(1, Count / 10f);
// add relative portion // add relative portion
amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); amount += (1 - non_relative_portion) * (Count / heatmap.PeakValue);
// apply easing // apply easing
amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
@ -318,15 +350,8 @@ namespace osu.Game.Rulesets.Osu.Statistics
Debug.Assert(amount <= 1); Debug.Assert(amount <= 1);
Alpha = Math.Min(amount / lighten_cutoff, 1); Alpha = Math.Min(amount / lighten_cutoff, 1);
if (pointType == HitPointType.Hit) Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
Colour = BaseColour.Lighten(Math.Max(0, amount - lighten_cutoff));
} }
} }
private enum HitPointType
{
Hit,
Miss
}
} }
} }

View File

@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -35,12 +35,16 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler; private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler;
// For osu! gameplay, everything is always on screen.
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public SmokeContainer Smoke { get; } public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; } public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); protected override GameplayCursorContainer? CreateCursor() => new OsuCursorContainer();
private readonly Container judgementAboveHitObjectLayer; private readonly Container judgementAboveHitObjectLayer;
@ -81,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI
public IHitPolicy HitPolicy public IHitPolicy HitPolicy
{ {
get => hitPolicy; get => hitPolicy;
[MemberNotNull(nameof(hitPolicy))]
set set
{ {
hitPolicy = value ?? throw new ArgumentNullException(nameof(value)); hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
@ -116,12 +121,12 @@ namespace osu.Game.Rulesets.Osu.UI
judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent); judgementAboveHitObjectLayer.Add(judgement.ProxiedAboveHitObjectsContent);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(OsuRulesetConfigManager config, IBeatmap beatmap) private void load(OsuRulesetConfigManager? config, IBeatmap? beatmap)
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
var osuBeatmap = (OsuBeatmap)beatmap; var osuBeatmap = (OsuBeatmap?)beatmap;
RegisterPool<HitCircle, DrawableHitCircle>(20, 100); RegisterPool<HitCircle, DrawableHitCircle>(20, 100);

View File

@ -4,7 +4,6 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>click the circles. to the beat.</Description> <Description>click the circles. to the beat.</Description>
<LangVersion>10</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Nuget"> <PropertyGroup Label="Nuget">

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
HitObjects = 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); 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);
}
} }
} }

View File

@ -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);
}
}

View File

@ -5,6 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override bool UserScrollSpeedAdjustment => false; protected override bool UserScrollSpeedAdjustment => false;
[CanBeNull]
private SkinnableDrawable scroller; private SkinnableDrawable scroller;
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
VisualisationMethod = ScrollVisualisationMethod.Overlapping; VisualisationMethod = ScrollVisualisationMethod.Overlapping;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load() private void load([CanBeNull] GameplayState gameplayState)
{ {
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar)); 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, FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
Depth = float.MaxValue {
}); RelativeSizeAxes = Axes.X,
Depth = float.MaxValue,
});
}
KeyBindingInputManager.Add(new DrumTouchInputArea()); KeyBindingInputManager.Add(new DrumTouchInputArea());
} }
@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
base.UpdateAfterChildren(); base.UpdateAfterChildren();
var playfieldScreen = Playfield.ScreenSpaceDrawQuad; 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) public MultiplierControlPoint ControlPointAt(double time)

View File

@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
CultureInfo.CurrentCulture = originalCulture; CultureInfo.CurrentCulture = originalCulture;
} }
private class TestLegacyScoreDecoder : LegacyScoreDecoder public class TestLegacyScoreDecoder : LegacyScoreDecoder
{ {
private readonly int beatmapVersion; private readonly int beatmapVersion;

View File

@ -0,0 +1,107 @@
// 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.IO.Legacy;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
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));
}
[Test]
public void ScoreWithMissIsNotPerfect()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 2,
[HitResult.Miss] = 1,
};
scoreInfo.MaximumStatistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 3
};
// Hit -> Miss -> Hit
scoreInfo.Combo = 1;
scoreInfo.MaxCombo = 1;
using (var ms = new MemoryStream())
{
new LegacyScoreEncoder(new Score { ScoreInfo = scoreInfo }, beatmap).Encode(ms, true);
ms.Seek(0, SeekOrigin.Begin);
using (var sr = new SerializationReader(ms))
{
sr.ReadByte(); // ruleset id
sr.ReadInt32(); // version
sr.ReadString(); // beatmap hash
sr.ReadString(); // username
sr.ReadString(); // score hash
sr.ReadInt16(); // count300
sr.ReadInt16(); // count100
sr.ReadInt16(); // count50
sr.ReadInt16(); // countGeki
sr.ReadInt16(); // countKatu
sr.ReadInt16(); // countMiss
sr.ReadInt32(); // total score
sr.ReadInt16(); // max combo
bool isPerfect = sr.ReadBoolean(); // full combo
Assert.That(isPerfect, Is.False);
}
}
}
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;
}
}
}

View File

@ -100,6 +100,7 @@ namespace osu.Game.Tests.NonVisual
public override Container FrameStableComponents { get; } public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; } public override IFrameStableClock FrameStableClock { get; }
internal override bool FrameStablePlayback { get; set; } internal override bool FrameStablePlayback { get; set; }
public override bool AllowBackwardsSeeks { get; set; }
public override IReadOnlyList<Mod> Mods { get; } public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; } public override double GameplayStartTime { get; }

View File

@ -173,6 +173,16 @@ namespace osu.Game.Tests.Skins.IO
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu); assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", 1.0m, osu);
}); });
[Test]
public Task TestImportWithSubfolder() => runSkinTest(async osu =>
{
const string filename = "Archives/skin-with-subfolder-zip-entries.osk";
var import = await loadSkinIntoOsu(osu, new ImportTask(TestResources.OpenResource(filename), filename));
assertCorrectMetadata(import, $"Totally fully features skin [Real Skin with Real Features] [{filename[..^4]}]", "Unknown", 2.7m, osu);
Assert.That(import.PerformRead(r => r.Files.Count), Is.EqualTo(3));
});
#endregion #endregion
#region Cases where imports should be uniquely imported #region Cases where imports should be uniquely imported

View File

@ -60,6 +60,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-argon-20231106.osk", "Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges // Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk", "Archives/modified-argon-20231108.osk",
// Covers "Argon" performance points counter
"Archives/modified-argon-20240305.osk",
}; };
/// <summary> /// <summary>

View File

@ -89,13 +89,18 @@ namespace osu.Game.Tests.Visual.Background
setupUserSettings(); setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false); AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false);
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); AddAssert("Background retained from song select", () =>
AddStep("Trigger background preview", () =>
{ {
InputManager.MoveMouseTo(playerLoader.ScreenPos); InputManager.MoveMouseTo(playerLoader);
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos); return songSelect.IsBackgroundCurrent();
}); });
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddUntilStep("Screen is dimmed and blur applied", () =>
{
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
return songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied();
});
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur)); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur));
} }

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -13,8 +13,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[SetUp] [SetUpSteps]
public void SetUp() => Schedule(() => public virtual void SetUpSteps()
{
AddStep("setup components", SetUpComponents);
}
public void SetUpComponents()
{ {
SetContents(skin => SetContents(skin =>
{ {
@ -28,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
implementation.Origin = Anchor.Centre; implementation.Origin = Anchor.Centre;
return implementation; return implementation;
}); });
}); }
protected abstract Drawable CreateDefaultImplementation(); protected abstract Drawable CreateDefaultImplementation();
protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation(); protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation();

View File

@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool AllowFail => false; protected override bool AllowFail => false;
protected override bool AllowBackwardsSeeks => true;
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {

View File

@ -130,8 +130,12 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
{
mainContainer.Child = new FrameStabilityContainer(gameplayStartTime) 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); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);

View File

@ -16,6 +16,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
{ {
protected override bool AllowBackwardsSeeks => true;
private bool seek;
[Test] [Test]
public void TestAllSamplesStopDuringSeek() public void TestAllSamplesStopDuringSeek()
{ {
@ -40,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
if (!samples.Any(s => s.Playing)) if (!samples.Any(s => s.Playing))
return false; return false;
Player.ChildrenOfType<GameplayClockContainer>().First().Seek(40000); seek = true;
return true; return true;
}); });
@ -53,10 +57,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample playback still disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); AddAssert("sample playback still disabled", () => sampleDisabler.SamplePlaybackDisabled.Value);
AddStep("stop seeking", () => seek = false);
AddUntilStep("seek finished, sample playback enabled", () => !sampleDisabler.SamplePlaybackDisabled.Value); AddUntilStep("seek finished, sample playback enabled", () => !sampleDisabler.SamplePlaybackDisabled.Value);
AddUntilStep("any sample is playing", () => Player.ChildrenOfType<PausableSkinnableSound>().Any(s => s.IsPlaying)); AddUntilStep("any sample is playing", () => Player.ChildrenOfType<PausableSkinnableSound>().Any(s => s.IsPlaying));
} }
protected override void Update()
{
base.Update();
if (seek)
{
// Frame stable playback is too fast to catch up these days.
//
// We want to keep seeking while asserting various test conditions, so
// continue to seek until we unset the flag.
var gameplayClockContainer = Player.ChildrenOfType<GameplayClockContainer>().First();
gameplayClockContainer.Seek(gameplayClockContainer.CurrentTime > 30000 ? 0 : 60000);
}
}
private IEnumerable<PausableSkinnableSound> allSounds => Player.ChildrenOfType<PausableSkinnableSound>(); private IEnumerable<PausableSkinnableSound> allSounds => Player.ChildrenOfType<PausableSkinnableSound>();
private IEnumerable<PausableSkinnableSound> allLoopingSounds => allSounds.Where(sound => sound.Looping); private IEnumerable<PausableSkinnableSound> allLoopingSounds => allSounds.Where(sound => sound.Looping);

View File

@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{ {
protected override bool AllowBackwardsSeeks => true;
private TestGameplaySampleTriggerSource sampleTriggerSource = null!; private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();

View File

@ -288,6 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override Container FrameStableComponents { get; } public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; } public override IFrameStableClock FrameStableClock { get; }
internal override bool FrameStablePlayback { get; set; } internal override bool FrameStablePlayback { get; set; }
public override bool AllowBackwardsSeeks { get; set; }
public override IReadOnlyList<Mod> Mods { get; } public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; } public override double GameplayStartTime { get; }

View File

@ -1,26 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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 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.Overlays;
using osu.Game.Users; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [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 Child = overlay = new MedalOverlay(),
{ RelativeSizeAxes = Axes.Both,
Name = @"Animations", CachedDependencies =
InternalName = @"all-intro-doubletime", [
Description = @"More complex than you think.", (typeof(IOverlayManager), overlayManagerMock.Object)
}), Add); ]
}); });
} }
[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)
})
}));
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private bool gameplayClockAlwaysGoingForward = true;
private double lastForwardCheckTime;
public TestScenePause() public TestScenePause()
{ {
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both }); base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmPausedWithNoOverlay(); 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] [Test]
public void TestPauseWithLargeOffset() public void TestPauseWithLargeOffset()
{ {
double lastStopTime;
bool alwaysGoingForward = true;
AddStep("force large offset", () => AddStep("force large offset", () =>
{ {
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset); var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
offset.Value = -5000; offset.Value = -5000;
}); });
AddStep("add time forward check hook", () => hookForwardPlaybackCheck();
{
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})");
};
});
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
resumeAndConfirm(); resumeAndConfirm();
AddAssert("time didn't go too far backwards", () => alwaysGoingForward); checkForwardPlayback();
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0)); 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] [Test]
public void TestPauseResume() public void TestPauseResume()
{ {

View File

@ -3,102 +3,89 @@
#nullable disable #nullable disable
using System; using System.Linq;
using System.Diagnostics;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osuTK; using osu.Game.Skinning.Triangles;
using osu.Game.Tests.Gameplay;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestScenePerformancePointsCounter : OsuTestScene public partial class TestScenePerformancePointsCounter : SkinnableHUDComponentTestScene
{ {
private DependencyProvidingContainer dependencyContainer; [Cached(typeof(ScoreProcessor))]
private readonly ScoreProcessor scoreProcessor = new OsuScoreProcessor();
private GameplayState gameplayState; [Cached]
private ScoreProcessor scoreProcessor; private readonly GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
private int iteration; private int iteration;
private Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private PerformancePointsCounter counter;
[SetUpSteps] protected override Drawable CreateDefaultImplementation() => new TrianglesPerformancePointsCounter();
public void SetUpSteps() => AddStep("create components", () => protected override Drawable CreateArgonImplementation() => new ArgonPerformancePointsCounter();
protected override Drawable CreateLegacyImplementation() => Empty();
private Bindable<JudgementResult> lastJudgementResult => (Bindable<JudgementResult>)gameplayState.LastJudgementResult;
public override void SetUpSteps()
{ {
var ruleset = CreateRuleset(); AddStep("reset", () =>
Debug.Assert(ruleset != null);
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo)
.GetPlayableBeatmap(ruleset.RulesetInfo);
lastJudgementResult = new Bindable<JudgementResult>();
gameplayState = new GameplayState(beatmap, ruleset);
gameplayState.LastJudgementResult.BindTo(lastJudgementResult);
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both, var ruleset = new OsuRuleset();
CachedDependencies = new (Type, object)[] var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo)
{ .GetPlayableBeatmap(ruleset.RulesetInfo);
(typeof(GameplayState), gameplayState),
(typeof(ScoreProcessor), scoreProcessor)
}
};
iteration = 0; iteration = 0;
}); scoreProcessor.ApplyBeatmap(beatmap);
lastJudgementResult.SetDefault();
});
protected override Ruleset CreateRuleset() => new OsuRuleset(); base.SetUpSteps();
}
private void createCounter() => AddStep("Create counter", () => [Test]
public void TestDisplay()
{ {
dependencyContainer.Child = counter = new PerformancePointsCounter AddSliderStep("pp", 0, 2000, 0, v => this.ChildrenOfType<PerformancePointsCounter>().ForEach(c => c.Current.Value = v));
{ AddToggleStep("toggle validity", v => this.ChildrenOfType<PerformancePointsCounter>().ForEach(c => c.IsValid = v));
Anchor = Anchor.Centre, }
Origin = Anchor.Centre,
Scale = new Vector2(5),
};
});
[Test] [Test]
public void TestBasicCounting() public void TestBasicCounting()
{ {
int previousValue = 0; int previousValue = 0;
createCounter();
AddAssert("counter displaying zero", () => counter.Current.Value == 0); AddAssert("counter displaying zero", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.Current.Value == 0));
AddRepeatStep("Add judgement", applyOneJudgement, 10); AddRepeatStep("Add judgement", applyOneJudgement, 10);
AddUntilStep("counter non-zero", () => counter.Current.Value > 0); AddUntilStep("counter non-zero", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.Current.Value > 0));
AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); AddUntilStep("counter valid", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.IsValid));
AddStep("Revert judgement", () => AddStep("Revert judgement", () =>
{ {
previousValue = counter.Current.Value; previousValue = this.ChildrenOfType<PerformancePointsCounter>().First().Current.Value;
scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement()));
}); });
AddUntilStep("counter decreased", () => counter.Current.Value < previousValue); AddUntilStep("counter decreased", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.Current.Value < previousValue));
AddStep("Add judgement", applyOneJudgement); AddStep("Add judgement", applyOneJudgement);
AddUntilStep("counter non-zero", () => counter.Current.Value > 0); AddUntilStep("counter non-zero", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.Current.Value > 0));
} }
[Test] [Test]
@ -106,10 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddRepeatStep("Add judgement", applyOneJudgement, 10); AddRepeatStep("Add judgement", applyOneJudgement, 10);
createCounter(); AddStep("recreate counter", SetUpComponents);
AddUntilStep("counter non-zero", () => counter.Current.Value > 0); AddUntilStep("counter non-zero", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.Current.Value > 0));
AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); AddUntilStep("counter valid", () => this.ChildrenOfType<PerformancePointsCounter>().All(c => c.IsValid));
} }
private void applyOneJudgement() private void applyOneJudgement()

View File

@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -36,7 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestPlayerLoader loader; private TestPlayerLoader loader;
private TestPlayer player; private TestPlayer player;
private bool epilepsyWarning; private bool? epilepsyWarning;
private BeatmapOnlineStatus? onlineStatus;
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
@ -81,7 +83,12 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
[SetUp] [SetUp]
public void Setup() => Schedule(() => player = null); public void Setup() => Schedule(() =>
{
player = null;
epilepsyWarning = null;
onlineStatus = null;
});
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
@ -118,8 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay
// Add intro time to test quick retry skipping (TestQuickRetry). // Add intro time to test quick retry skipping (TestQuickRetry).
workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; workingBeatmap.BeatmapInfo.AudioLeadIn = 60000;
// Turn on epilepsy warning to test warning display (TestEpilepsyWarning). // Set up data for testing disclaimer display.
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning; workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false;
workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked;
Beatmap.Value = workingBeatmap; Beatmap.Value = workingBeatmap;
@ -334,13 +342,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning); AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(warning ? 1 : 0));
if (warning)
{
AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
}
restoreVolumes(); restoreVolumes();
} }
@ -357,30 +359,45 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("epilepsy warning absent", () => getWarning() == null); AddUntilStep("epilepsy warning absent", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Single().Alpha, () => Is.Zero);
restoreVolumes(); restoreVolumes();
} }
[Test] [TestCase(BeatmapOnlineStatus.Loved, 1)]
public void TestEpilepsyWarningEarlyExit() [TestCase(BeatmapOnlineStatus.Qualified, 1)]
[TestCase(BeatmapOnlineStatus.Graveyard, 0)]
public void TestStatusWarning(BeatmapOnlineStatus status, int expectedDisclaimerCount)
{ {
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("disable epilepsy warning", () => epilepsyWarning = false);
AddStep("set beatmap status", () => onlineStatus = status);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0); AddAssert($"disclaimer count is {expectedDisclaimerCount}", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(expectedDisclaimerCount));
AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
AddStep("exit early", () => loader.Exit()); restoreVolumes();
}
AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden); [Test]
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); public void TestCombinedWarnings()
{
saveVolumes();
setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("disable epilepsy warning", () => epilepsyWarning = true);
AddStep("set beatmap status", () => onlineStatus = BeatmapOnlineStatus.Loved);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("disclaimer count is 2", () => this.ChildrenOfType<PlayerLoaderDisclaimer>().Count(), () => Is.EqualTo(2));
restoreVolumes(); restoreVolumes();
} }
@ -479,8 +496,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick()); AddStep("click notification", () => notification.TriggerClick());
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader private partial class TestPlayerLoader : PlayerLoader
{ {
public new VisualSettings VisualSettings => base.VisualSettings; public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TextureUpload upscale(TextureUpload textureUpload) 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. // The original texture upload will no longer be returned or used.
textureUpload.Dispose(); textureUpload.Dispose();

View File

@ -269,6 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo)); drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
drawableRuleset.FrameStablePlayback = true; drawableRuleset.FrameStablePlayback = true;
drawableRuleset.AllowBackwardsSeeks = true;
drawableRuleset.PoolSize = poolSize; drawableRuleset.PoolSize = poolSize;
Child = new Container Child = new Container

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -21,10 +20,11 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();
[SetUpSteps] public override void SetUpSteps()
public void SetUpSteps()
{ {
AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1); AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1);
base.SetUpSteps();
} }
[Test] [Test]

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
@ -25,14 +24,15 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) }; protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) }; protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };
[SetUpSteps] public override void SetUpSteps()
public void SetUpSteps()
{ {
AddStep(@"Reset all", delegate AddStep(@"Reset all", delegate
{ {
healthProcessor.Health.Value = 1; healthProcessor.Health.Value = 1;
healthProcessor.Failed += () => false; // health won't be updated if the processor gets into a "fail" state. healthProcessor.Failed += () => false; // health won't be updated if the processor gets into a "fail" state.
}); });
base.SetUpSteps();
} }
protected override void Update() protected override void Update()

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestSkinSourceContainer skinSource = null!; private TestSkinSourceContainer skinSource = null!;
private PausableSkinnableSound skinnableSound = null!; private PausableSkinnableSound skinnableSound = null!;
private const string sample_lookup = "Gameplay/normal-sliderslide"; private const string sample_lookup = "Gameplay/Argon/normal-sliderslide";
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()

View File

@ -31,6 +31,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
protected override bool AllowBackwardsSeeks => true;
protected new OutroPlayer Player => (OutroPlayer)base.Player; protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentBeatmapDuration; private double currentBeatmapDuration;

View File

@ -22,6 +22,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
@ -286,6 +287,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
} }
[Test]
[FlakyTest] // See above
public void TestModSelectOverlay()
{
AddStep("add playlist item", () =>
{
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 2.0 } }),
new APIMod(new OsuModStrictTracking()),
},
AllowedMods = new[]
{
new APIMod(new OsuModFlashlight()),
}
});
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<RoomSubScreen.UserModSelectButton>();
AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{ {
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]

View File

@ -6,6 +6,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Configuration;
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Leaderboards; 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;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); 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] [Test]
public void TestRetryFromResults() public void TestRetryFromResults()
{ {

View File

@ -14,7 +14,6 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Users; using osu.Game.Users;
@ -142,7 +141,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo())); AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(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] [Test]

View 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
})
});
}
}
}

View File

@ -88,8 +88,20 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any()); AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
} }
private void showPanel(ScoreInfo score) => [Test]
Child = new ExpandedPanelMiddleContentContainer(score); public void TestFailedSDisplay([Values] bool withFlair)
{
AddStep("show failed S score", () =>
{
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
score.Rank = ScoreRank.A;
score.Accuracy = 0.975;
showPanel(score, withFlair);
});
}
private void showPanel(ScoreInfo score, bool withFlair = false) =>
Child = new ExpandedPanelMiddleContentContainer(score, withFlair);
private BeatmapInfo createTestBeatmap([NotNull] RealmUser author) private BeatmapInfo createTestBeatmap([NotNull] RealmUser author)
{ {
@ -107,7 +119,7 @@ namespace osu.Game.Tests.Visual.Ranking
private partial class ExpandedPanelMiddleContentContainer : Container private partial class ExpandedPanelMiddleContentContainer : Container
{ {
public ExpandedPanelMiddleContentContainer(ScoreInfo score) public ExpandedPanelMiddleContentContainer(ScoreInfo score, bool withFlair)
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -119,7 +131,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"), Colour = Color4Extensions.FromHex("#444"),
}, },
new ExpandedPanelMiddleContent(score) new ExpandedPanelMiddleContent(score, withFlair)
}; };
} }
} }

View File

@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Mods = { BindTarget = SelectedMods },
}); });
AddStep("set beatmap", () => AddStep("set beatmap", () =>

View File

@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); () => 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", () => private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any() modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded) && modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)

View File

@ -80,6 +80,8 @@ namespace osu.Game.Audio
yield return $"Gameplay/{Bank}-{Name}{Suffix}"; yield return $"Gameplay/{Bank}-{Name}{Suffix}";
yield return $"Gameplay/{Bank}-{Name}"; yield return $"Gameplay/{Bank}-{Name}";
yield return $"Gameplay/{Name}";
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
// The original texture upload will no longer be returned or used. // The original texture upload will no longer be returned or used.
textureUpload.Dispose(); textureUpload.Dispose();
Size size = image.Size(); Size size = image.Size;
// Assume that panel backgrounds are always displayed using `FillMode.Fill`. // Assume that panel backgrounds are always displayed using `FillMode.Fill`.
// Also assume that all backgrounds are wider than they are tall, so the // Also assume that all backgrounds are wider than they are tall, so the

View File

@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Dimmed.BindValueChanged(_ => updateState()); Dimmed.BindValueChanged(_ => updateState());
playButton.Playing.BindValueChanged(_ => updateState(), true); playButton.Playing.BindValueChanged(_ => updateState(), true);
((IBindable<double>)progress.Current).BindTo(playButton.Progress);
FinishTransforms(true); FinishTransforms(true);
} }
protected override void Update()
{
base.Update();
progress.Progress = playButton.Progress.Value;
}
private void updateState() private void updateState()
{ {
bool shouldDim = Dimmed.Value || playButton.Playing.Value; bool shouldDim = Dimmed.Value || playButton.Playing.Value;

View File

@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
downloadTracker, downloadTracker,
background = new Container background = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Child = new Box Child = new Box
@ -165,9 +165,13 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState() private void updateState()
{ {
float targetWidth = Width - (ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth); float buttonAreaWidth = ShowDetails.Value ? ButtonsExpandedWidth : ButtonsCollapsedWidth;
float mainAreaWidth = Width - buttonAreaWidth;
mainArea.ResizeWidthTo(targetWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); mainArea.ResizeWidthTo(mainAreaWidth, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
background.ResizeWidthTo(buttonAreaWidth + BeatmapCard.CORNER_RADIUS, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); background.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); buttons.FadeTo(ShowDetails.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; 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; break;
} }

View File

@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
private IDisposable? beatmapOffsetSubscription; private IDisposable? beatmapOffsetSubscription;
private readonly DecouplingFramedClock decoupledTrack; private readonly DecouplingFramedClock decoupledTrack;
private readonly InterpolatingFramedClock interpolatedTrack;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; 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 // 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). // 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) if (applyOffsets)
{ {
@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
base.Dispose(isDisposing); base.Dispose(isDisposing);
beatmapOffsetSubscription?.Dispose(); 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}";
}
}
} }
} }

View File

@ -23,8 +23,7 @@ namespace osu.Game.Beatmaps.Timing
public TimeSignature(int numerator) public TimeSignature(int numerator)
{ {
if (numerator < 1) ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numerator);
throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive.");
Numerator = numerator; Numerator = numerator;
} }

View File

@ -75,7 +75,7 @@ namespace osu.Game.Configuration
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false); SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false);
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled =>
{ {
if (enabled.NewValue) if (enabled.NewValue)
SetValue(OsuSetting.SaveUsername, true); SetValue(OsuSetting.SaveUsername, true);

View File

@ -489,8 +489,7 @@ namespace osu.Game.Database
/// <param name="action">The work to run.</param> /// <param name="action">The work to run.</param>
public Task WriteAsync(Action<Realm> action) public Task WriteAsync(Action<Realm> action)
{ {
if (isDisposed) ObjectDisposedException.ThrowIf(isDisposed, this);
throw new ObjectDisposedException(nameof(RealmAccess));
// Required to ensure the write is tracked and accounted for before disposal. // Required to ensure the write is tracked and accounted for before disposal.
// Can potentially be avoided if we have a need to do so in the future. // Can potentially be avoided if we have a need to do so in the future.
@ -675,8 +674,7 @@ namespace osu.Game.Database
private Realm getRealmInstance() private Realm getRealmInstance()
{ {
if (isDisposed) ObjectDisposedException.ThrowIf(isDisposed, this);
throw new ObjectDisposedException(nameof(RealmAccess));
bool tookSemaphoreLock = false; bool tookSemaphoreLock = false;
@ -1189,8 +1187,7 @@ namespace osu.Game.Database
if (!ThreadSafety.IsUpdateThread) if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
if (isDisposed) ObjectDisposedException.ThrowIf(isDisposed, this);
throw new ObjectDisposedException(nameof(RealmAccess));
SynchronizationContext? syncContext = null; SynchronizationContext? syncContext = null;

View File

@ -415,7 +415,7 @@ namespace osu.Game.Database
// Calculate how many times the longest combo the user has achieved in the play can repeat // 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. // 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 maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1; double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
@ -426,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. // ...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 remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
double lowerEstimateOfComboPortionInStandardisedScore double scoreBasedEstimateOfComboPortionInStandardisedScore
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
+ remainingComboPortionInStandardisedScore; + remainingComboPortionInStandardisedScore;
// Compute approximate upper estimate new score for that play. // 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. // 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; remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss); double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1` // Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
@ -449,7 +448,17 @@ namespace osu.Game.Database
// we can skip adding the 1 and just multiply by x ^ 0.5. // we can skip adding the 1 and just multiply by x ^ 0.5.
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT); 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. // Approximate by combining lower and upper estimates.
// As the lower-estimate is very pessimistic, we use a 30/70 ratio // As the lower-estimate is very pessimistic, we use a 30/70 ratio

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers
[Cached(typeof(IPreviewTrackOwner))] [Cached(typeof(IPreviewTrackOwner))]
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction> public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
{ {
private Sample samplePopIn; protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private Sample samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string? PopInSampleName => @"UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out"; protected virtual string? PopOutSampleName => @"UI/overlay-pop-out";
protected virtual double PopInOutSampleBalance => 0; protected virtual double PopInOutSampleBalance => 0;
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
@ -34,19 +32,23 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
protected virtual bool DimMainContent => true; protected virtual bool DimMainContent => true;
[Resolved(CanBeNull = true)] [Resolved]
private IOverlayManager overlayManager { get; set; } private IOverlayManager? overlayManager { get; set; }
[Resolved] [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)] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager? audio)
{ {
samplePopIn = audio.Samples.Get(PopInSampleName); if (!string.IsNullOrEmpty(PopInSampleName))
samplePopOut = audio.Samples.Get(PopOutSampleName); samplePopIn = audio?.Samples.Get(PopInSampleName);
if (!string.IsNullOrEmpty(PopOutSampleName))
samplePopOut = audio?.Samples.Get(PopOutSampleName);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor
if (dragRotationState == DragRotationState.Rotating && distance > 0) if (dragRotationState == DragRotationState.Rotating && distance > 0)
{ {
Vector2 offset = e.MousePosition - positionMouseDown; 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 // Always rotate in the direction of least distance
float diff = (degrees - activeCursor.Rotation) % 360; float diff = (degrees - activeCursor.Rotation) % 360;

View File

@ -63,8 +63,12 @@ namespace osu.Game.Graphics
case ScoreRank.C: case ScoreRank.C:
return Color4Extensions.FromHex(@"ff8e5d"); return Color4Extensions.FromHex(@"ff8e5d");
default: case ScoreRank.D:
return Color4Extensions.FromHex(@"ff5a5a"); return Color4Extensions.FromHex(@"ff5a5a");
case ScoreRank.F:
default:
return Color4Extensions.FromHex(@"3f3f3f");
} }
} }

View File

@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public partial class OsuNumberBox : OsuTextBox public partial class OsuNumberBox : OsuTextBox
{ {
protected override bool AllowIme => false; protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit(); protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
} }
} }

View File

@ -116,18 +116,18 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private const float transition_length = 500; protected const float TRANSITION_LENGTH = 500;
protected void FadeHovered() protected virtual void FadeHovered()
{ {
Bar.FadeIn(transition_length, Easing.OutQuint); Bar.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint); Text.FadeColour(Color4.White, TRANSITION_LENGTH, Easing.OutQuint);
} }
protected void FadeUnhovered() protected virtual void FadeUnhovered()
{ {
Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint); Bar.FadeTo(IsHovered ? 1 : 0, TRANSITION_LENGTH, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint); Text.FadeColour(IsHovered ? Color4.White : AccentColour, TRANSITION_LENGTH, Easing.OutQuint);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Toolkit.HighPerformance; using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size); var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
using (Stream s = entry.OpenEntryStream()) using (Stream s = entry.OpenEntryStream())
s.ReadToFill(owner.Memory.Span); s.ReadExactly(owner.Memory.Span);
return new MemoryOwnerMemoryStream(owner); return new MemoryOwnerMemoryStream(owner);
} }
@ -47,7 +46,7 @@ namespace osu.Game.IO.Archives
archiveStream.Dispose(); archiveStream.Dispose();
} }
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); public override IEnumerable<string> Filenames => archive.Entries.Where(e => !e.IsDirectory).Select(e => e.Key).ExcludeSystemFileNames();
private class MemoryOwnerMemoryStream : Stream private class MemoryOwnerMemoryStream : Stream
{ {

View File

@ -80,8 +80,7 @@ namespace osu.Game.IO
public override Storage GetStorageForDirectory(string path) public override Storage GetStorageForDirectory(string path)
{ {
if (string.IsNullOrEmpty(path)) ArgumentException.ThrowIfNullOrEmpty(path);
throw new ArgumentException("Must be non-null and not empty string", nameof(path));
if (!path.EndsWith(Path.DirectorySeparatorChar)) if (!path.EndsWith(Path.DirectorySeparatorChar))
path += Path.DirectorySeparatorChar; path += Path.DirectorySeparatorChar;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Performance points will not be granted due to active mods." /// "Performance points will not be granted due to active mods."
/// </summary> /// </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}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }

View File

@ -0,0 +1,48 @@
// 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 PlayerLoaderStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.PlayerLoader";
/// <summary>
/// "This beatmap contains scenes with rapidly flashing colours"
/// </summary>
public static LocalisableString EpilepsyWarningTitle => new TranslatableString(getKey(@"epilepsy_warning_title"), @"This beatmap contains scenes with rapidly flashing colours");
/// <summary>
/// "Please take caution if you are affected by epilepsy."
/// </summary>
public static LocalisableString EpilepsyWarningContent => new TranslatableString(getKey(@"epilepsy_warning_content"), @"Please take caution if you are affected by epilepsy.");
/// <summary>
/// "This beatmap is loved"
/// </summary>
public static LocalisableString LovedBeatmapDisclaimerTitle => new TranslatableString(getKey(@"loved_beatmap_disclaimer_title"), @"This beatmap is loved");
/// <summary>
/// "No performance points will be awarded.
/// Leaderboards may be reset by the beatmap creator."
/// </summary>
public static LocalisableString LovedBeatmapDisclaimerContent => new TranslatableString(getKey(@"loved_beatmap_disclaimer_content"), @"No performance points will be awarded.
Leaderboards may be reset by the beatmap creator.");
/// <summary>
/// "This beatmap is qualified"
/// </summary>
public static LocalisableString QualifiedBeatmapDisclaimerTitle => new TranslatableString(getKey(@"qualified_beatmap_disclaimer_title"), @"This beatmap is qualified");
/// <summary>
/// "No performance points will be awarded.
/// Leaderboards will be reset when the beatmap is ranked."
/// </summary>
public static LocalisableString QualifiedBeatmapDisclaimerContent => new TranslatableString(getKey(@"qualified_beatmap_disclaimer_content"), @"No performance points will be awarded.
Leaderboards will be reset when the beatmap is ranked.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View 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}";
}
}

View File

@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses
RulesetID = score.RulesetID, RulesetID = score.RulesetID,
Passed = score.Passed, Passed = score.Passed,
Mods = score.APIMods, Mods = score.APIMods,
Statistics = score.Statistics.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(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
}; };
} }
} }

View File

@ -13,6 +13,8 @@ using osu.Framework.Logging;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.Notifications.WebSocket; 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 namespace osu.Game.Online.Chat
{ {

View File

@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards
case ScoreRank.C: case ScoreRank.C:
return Color4Extensions.FromHex(@"473625"); return Color4Extensions.FromHex(@"473625");
default: case ScoreRank.D:
return Color4Extensions.FromHex(@"512525"); return Color4Extensions.FromHex(@"512525");
case ScoreRank.F:
default:
return Color4Extensions.FromHex(@"CC3333");
} }
} }
} }

View File

@ -8,7 +8,7 @@ using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
namespace osu.Game.Online.Notifications.WebSocket namespace osu.Game.Online.Notifications.WebSocket.Events
{ {
/// <summary> /// <summary>
/// A websocket message sent from the server when new messages arrive. /// A websocket message sent from the server when new messages arrive.

View 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 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; }
}
}

View File

@ -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 string? 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; }
}
}

Some files were not shown because too many files have changed in this diff Show More