1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 12:35:34 +08:00

Merge branch 'master' into sb-lifetime-improvements

This commit is contained in:
Dean Herbert 2024-03-06 19:27:40 +08:00 committed by GitHub
commit 13f7480d79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 2022 additions and 455 deletions

View File

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

View File

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

View File

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

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">
<EmbeddedResource Include="lazer.ico" />
</ItemGroup>
<ItemGroup Label="Windows Icons">
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

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

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;
double startTime = hitObject.StartTime;
if (lastPosition == null)
if (lastPosition == null ||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
// todo: should be revisited and corrected later probably.
lastPosition == 0)
{
lastPosition = offsetPosition;
lastStartTime = startTime;

View File

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

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
// matches stable.
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
// the above early-return skips the failure check at the end of the same method:
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
// making it impossible to fail on a tiny droplet regardless of result.
if (result.Type == HitResult.SmallTickMiss)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
// No matter what, mania doesn't have passive HP drain.
Assert.That(processor.DrainRate, Is.Zero);
}
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new Note(), 0.01, true],
[new HeadNote(), 0.01, true],
[new TailNote(), 0.01, true],
[new HoldNoteBody(), 0, true], // hold note break
[new HoldNote(), 0, true],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup hierarchy", () => Child = new Container
AddStep("setup hierarchy", () =>
{
Clock = new FramedClock(clock = new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
Child = new Container
{
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
}
Clock = new FramedClock(clock = new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
}
};
drawableRuleset.AllowBackwardsSeeks = true;
});
AddStep("retrieve config bindable", () =>
{

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

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
// some spinners may not complete due to very minor decimal loss during calculation
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
}
}
}

View File

@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;

View File

@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// 0.05 rad/ms, or ~477 RPM, as per stable.
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000;
switch (h)
{

View File

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

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Current = { Value = arc_fill },
Progress = arc_fill,
Rotation = -arc_fill * 180,
InnerRadius = arc_radius,
RoundedCaps = true,
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.Update();
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
fill.Rotation = (float)(-fill.Current.Value * 180);
fill.Rotation = (float)(-fill.Progress * 180);
}
}
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (mousePosition is Vector2 pos)
{
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
// Normalise the delta to -180 .. 180

View File

@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
//
// We also need to apply the anti-clockwise rotation.
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitObjects =
{
new DrumRoll { Duration = 2000 }
new Swell { Duration = 2000 }
}
};
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestMissHitAndHitSwell()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Swell { Duration = 2000 }
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
{
var nestedJudgement = nested.CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
}
var judgement = beatmap.HitObjects[1].CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
Assert.That(healthProcessor.HasFailed, Is.True);
});
}
private static readonly object[][] test_cases =
[
// hitobject, fail expected after miss
[new Hit(), true],
[new Hit.StrongNestedHit(new Hit()), false],
[new DrumRollTick(new DrumRoll()), false],
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
[new DrumRoll(), false],
[new SwellTick(), false],
[new Swell(), false]
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

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.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override bool UserScrollSpeedAdjustment => false;
[CanBeNull]
private SkinnableDrawable scroller;
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
}
[BackgroundDependencyLoader]
private void load()
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] GameplayState gameplayState)
{
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
.SelectMany(l => l.Elements)
.OfType<StoryboardSprite>()
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
if (spriteElements.Count() < 10)
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue
});
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
{
RelativeSizeAxes = Axes.X,
Depth = float.MaxValue,
});
}
KeyBindingInputManager.Add(new DrumTouchInputArea());
}
@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
base.UpdateAfterChildren();
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
if (scroller != null)
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
}
public MultiplierControlPoint ControlPointAt(double time)

View File

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

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
{
public class LegacyScoreEncoderTest
{
[TestCase(1, 3)]
[TestCase(1, 0)]
[TestCase(0, 3)]
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
{
var ruleset = new CatchRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 50,
[HitResult.LargeTickHit] = 5,
[HitResult.Miss] = missCount,
[HitResult.LargeTickMiss] = largeTickMissCount
};
var score = new Score { ScoreInfo = scoreInfo };
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
var decodedAfterEncode = decoder.Parse(decodeStream);
return decodedAfterEncode;
}
}
}

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
{
protected override bool AllowBackwardsSeeks => true;
[Test]
public void TestAllSamplesStopDuringSeek()
{

View File

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

View File

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

View File

@ -1,26 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public partial class TestSceneMedalOverlay : OsuTestScene
public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
{
public TestSceneMedalOverlay()
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private MedalOverlay overlay = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep(@"display", () =>
var overlayManagerMock = new Mock<IOverlayManager>();
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
AddStep("create overlay", () => Child = new DependencyProvidingContainer
{
LoadComponentAsync(new MedalOverlay(new Medal
{
Name = @"Animations",
InternalName = @"all-intro-doubletime",
Description = @"More complex than you think.",
}), Add);
Child = overlay = new MedalOverlay(),
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(IOverlayManager), overlayManagerMock.Object)
]
});
}
[Test]
public void TestBasicAward()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
}
[Test]
public void TestMultipleMedalsInQuickSuccession()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
awardMedal(new UserAchievementUnlock
{
Title = "S-Ranker",
Description = "Accuracy is really underrated.",
Slug = @"all-secret-rank-s"
});
awardMedal(new UserAchievementUnlock
{
Title = "500 Combo",
Description = "500 big ones! You're moving up in the world!",
Slug = @"osu-combo-500"
});
}
[Test]
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
{
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
}
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(unlock)
})
}));
}
}

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Container<Drawable> Content => content;
private bool gameplayClockAlwaysGoingForward = true;
private double lastForwardCheckTime;
public TestScenePause()
{
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmPausedWithNoOverlay();
}
[Test]
public void TestForwardPlaybackGuarantee()
{
hookForwardPlaybackCheck();
AddUntilStep("wait for forward playback", () => Player.GameplayClockContainer.CurrentTime > 1000);
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
checkForwardPlayback();
}
[Test]
public void TestPauseWithLargeOffset()
{
double lastStopTime;
bool alwaysGoingForward = true;
AddStep("force large offset", () =>
{
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
offset.Value = -5000;
});
AddStep("add time forward check hook", () =>
{
lastStopTime = double.MinValue;
alwaysGoingForward = true;
Player.OnUpdate += _ =>
{
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
double currentTime = masterClock.CurrentTime;
bool goingForward = currentTime >= lastStopTime;
alwaysGoingForward &= goingForward;
if (!goingForward)
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
};
});
hookForwardPlaybackCheck();
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
resumeAndConfirm();
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
checkForwardPlayback();
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
private void checkForwardPlayback() => AddAssert("time didn't go too far backwards", () => gameplayClockAlwaysGoingForward);
private void hookForwardPlaybackCheck()
{
AddStep("add time forward check hook", () =>
{
lastForwardCheckTime = double.MinValue;
gameplayClockAlwaysGoingForward = true;
Player.OnUpdate += _ =>
{
var frameStableClock = Player.ChildrenOfType<FrameStabilityContainer>().Single().Clock;
double currentTime = frameStableClock.CurrentTime;
bool goingForward = currentTime >= lastForwardCheckTime;
lastForwardCheckTime = currentTime;
gameplayClockAlwaysGoingForward &= goingForward;
if (!goingForward)
Logger.Log($"Went too far backwards (last stop: {lastForwardCheckTime:N1} current: {currentTime:N1})");
};
});
}
[Test]
public void TestPauseResume()
{

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TextureUpload upscale(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();

View File

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

View File

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

View File

@ -6,6 +6,7 @@
using System;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
}
[Test]
public void TestShowMedalAtResults()
{
playToResults();
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
})
})
}));
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestRetryFromResults()
{

View File

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

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

@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
[Test]
public void TestModSettingsOrder()
{
createScreen();
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
AddAssert("mod settings order: DT, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList());
AddAssert("mod settings order: NC, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
return columns.ElementAt(0).Mod is OsuModNightcore &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)

View File

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

View File

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

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.Storyboards;
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
break;
}

View File

@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
private IDisposable? beatmapOffsetSubscription;
private readonly DecouplingFramedClock decoupledTrack;
private readonly InterpolatingFramedClock interpolatedTrack;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
if (applyOffsets)
{
@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
base.Dispose(isDisposing);
beatmapOffsetSubscription?.Dispose();
}
public string GetSnapshot()
{
return
$"originalSource: {output(Source)}\n" +
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
$"decoupledTrack: {output(decoupledTrack)}\n" +
$"finalClockSource: {output(finalClockSource)}\n";
string output(IClock? clock)
{
if (clock == null)
return "null";
if (clock is IFrameBasedClock framed)
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
}
}
}
}

View File

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

View File

@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor
if (dragRotationState == DragRotationState.Rotating && distance > 0)
{
Vector2 offset = e.MousePosition - positionMouseDown;
float degrees = MathUtils.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
float degrees = float.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
// Always rotate in the direction of least distance
float diff = (degrees - activeCursor.Rotation) % 360;

View File

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

View File

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

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
using SixLabors.ImageSharp.Memory;
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
using (Stream s = entry.OpenEntryStream())
s.ReadToFill(owner.Memory.Span);
s.ReadExactly(owner.Memory.Span);
return new MemoryOwnerMemoryStream(owner);
}

View File

@ -67,7 +67,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Performance points will not be granted due to active mods."
/// </summary>
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points will not be granted due to active mods.");
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods.");
private static string getKey(string key) => $@"{prefix}:{key}";
}

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,
Passed = score.Passed,
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
};
}
}

View File

@ -13,6 +13,8 @@ using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Online.Notifications.WebSocket.Requests;
namespace osu.Game.Online.Chat
{

View File

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

View File

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

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 ushort? AchievementMode { get; set; }
[JsonProperty("cover_url")]
public string CoverUrl { get; set; } = string.Empty;
[JsonProperty("slug")]
public string Slug { get; set; } = string.Empty;
[JsonProperty("title")]
public string Title { get; set; } = string.Empty;
[JsonProperty("description")]
public string Description { get; set; } = string.Empty;
[JsonProperty("user_id")]
public uint UserId { get; set; }
}
}

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket
namespace osu.Game.Online.Notifications.WebSocket.Requests
{
/// <summary>
/// A websocket message notifying the server that the client no longer wants to receive chat messages.

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket
namespace osu.Game.Online.Notifications.WebSocket.Requests
{
/// <summary>
/// A websocket message notifying the server that the client wants to receive chat messages.

View File

@ -1083,6 +1083,7 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
@ -1190,6 +1191,9 @@ namespace osu.Game
{
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
if (entry.Exception is SentryOnlyDiagnosticsException)
return;
const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)

View File

@ -0,0 +1,312 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Shapes;
using System;
using System.Diagnostics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
namespace osu.Game.Overlays
{
public partial class MedalAnimation : VisibilityContainer
{
public const float DISC_SIZE = 400;
private const float border_width = 5;
private readonly Medal medal;
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal? drawableMedal;
private Sample? getSample;
private readonly Container content;
public MedalAnimation(Medal medal)
{
this.medal = medal;
RelativeSizeAxes = Axes.Both;
Child = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
{
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 50,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
}, loaded =>
{
disc.Add(loaded);
startAnimation();
});
}
protected override void Update()
{
base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample?.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
{
disc.FadeIn(initial_duration)
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
Debug.Assert(drawableMedal != null);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
}
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut()
{
this.FadeOut(200);
}
public void Dismiss()
{
if (drawableMedal != null && drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
}
Hide();
Expire();
}
private partial class BackgroundStrip : Container
{
public BackgroundStrip(float start, float end)
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
}
}
}

View File

@ -1,324 +1,130 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osuTK.Input;
using osu.Framework.Graphics.Shapes;
using System;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Users;
namespace osu.Game.Overlays
{
public partial class MedalOverlay : FocusedOverlayContainer
public partial class MedalOverlay : OsuFocusedOverlayContainer
{
public const float DISC_SIZE = 400;
protected override string? PopInSampleName => null;
protected override string? PopOutSampleName => null;
private const float border_width = 5;
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly Medal medal;
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal drawableMedal;
protected override void PopIn() => this.FadeIn();
private Sample getSample;
protected override void PopOut() => this.FadeOut();
private readonly Container content;
private readonly Queue<MedalAnimation> queuedMedals = new Queue<MedalAnimation>();
public MedalOverlay(Medal medal)
{
this.medal = medal;
RelativeSizeAxes = Axes.Both;
[Resolved]
private IAPIProvider api { get; set; } = null!;
Child = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
private Container<Drawable> medalContainer = null!;
private MedalAnimation? lastAnimation;
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
private void load()
{
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
RelativeSizeAxes = Axes.Both;
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
api.NotificationsClient.MessageReceived += handleMedalMessages;
Add(medalContainer = new Container
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 50,
};
RelativeSizeAxes = Axes.Both
});
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
OverlayActivationMode.BindValueChanged(val =>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
}, loaded =>
if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false))
Show();
}, true);
}
private void handleMedalMessages(SocketMessage obj)
{
if (obj.Event != @"new")
return;
var data = obj.Data?.ToObject<NewPrivateNotificationEvent>();
if (data == null || data.Name != @"user_achievement_unlock")
return;
var details = data.Details?.ToObject<UserAchievementUnlock>();
if (details == null)
return;
var medal = new Medal
{
disc.Add(loaded);
startAnimation();
});
Name = details.Title,
InternalName = details.Slug,
Description = details.Description,
};
var medalAnimation = new MedalAnimation(medal);
queuedMedals.Enqueue(medalAnimation);
if (OverlayActivationMode.Value == OverlayActivation.All)
Scheduler.AddOnce(Show);
}
protected override void Update()
{
base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
if (medalContainer.Any() || lastAnimation?.IsLoaded == false)
return;
if (!queuedMedals.TryDequeue(out lastAnimation))
{
Hide();
return;
}
LoadComponentAsync(lastAnimation, medalContainer.Add);
}
protected override bool OnClick(ClickEvent e)
{
dismiss();
lastAnimation?.Dismiss();
return true;
}
protected override void OnFocusLost(FocusLostEvent e)
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.CurrentState.Keyboard.Keys.IsPressed(Key.Escape)) dismiss();
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
if (e.Action == GlobalAction.Back)
{
disc.FadeIn(initial_duration)
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
}
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut()
{
this.FadeOut(200);
}
private void dismiss()
{
if (drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
lastAnimation?.Dismiss();
return true;
}
Hide();
Expire();
return base.OnPressed(e);
}
private partial class BackgroundStrip : Container
protected override void Dispose(bool isDisposing)
{
public BackgroundStrip(float start, float end)
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
base.Dispose(isDisposing);
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
if (api.IsNotNull())
api.NotificationsClient.MessageReceived -= handleMedalMessages;
}
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.MedalSplash
public DrawableMedal(Medal medal)
{
this.medal = medal;
Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2);
Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2);
FillFlowContainer infoFlow;
Children = new Drawable[]
@ -174,7 +174,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1);
this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
unlocked.FadeInFromZero(duration);
break;
@ -184,7 +184,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1);
this.ScaleTo(scale_when_full, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
unlocked.Show();
name.FadeInFromZero(duration + 100);
description.FadeInFromZero(duration * 2);

View File

@ -86,7 +86,10 @@ namespace osu.Game.Overlays.Mods
{
modSettingsFlow.Clear();
foreach (var mod in SelectedMods.Value.AsOrdered())
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
// Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent),
// which breaks user expectations when interacting with the overlay.
foreach (var mod in SelectedMods.Value)
{
var settings = mod.CreateSettingsControls().ToList();
@ -110,10 +113,14 @@ namespace osu.Game.Overlays.Mods
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
private partial class ModSettingsColumn : CompositeDrawable
public partial class ModSettingsColumn : CompositeDrawable
{
public readonly Mod Mod;
public ModSettingsColumn(Mod mod, IEnumerable<Drawable> settingsControls)
{
Mod = mod;
Width = 250;
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding { Bottom = 7 };

View File

@ -10,6 +10,7 @@ using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Statistics;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
@ -107,6 +108,9 @@ namespace osu.Game.Overlays.Settings.Sections.General
try
{
GlobalStatistics.OutputToLog();
Logger.Flush();
var logStorage = Logger.Storage;
using (var outStream = storage.CreateFileSafely(archive_filename))

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@ -196,7 +195,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
var matrix = Matrix3.Identity;
MatrixExtensions.TranslateFromLeft(ref matrix, offset);
MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value));
MatrixExtensions.RotateFromLeft(ref matrix, float.DegreesToRadians(rotation.Value));
usableAreaQuad *= matrix;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@ -69,7 +68,7 @@ namespace osu.Game.Overlays.Settings
{
protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
public new void NotifyInputError() => base.NotifyInputError();
}

View File

@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Volume
Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true);
bgProgress.Current.Value = 0.75f;
bgProgress.Progress = 0.75f;
}
private int? displayVolumeInt;
@ -265,8 +265,8 @@ namespace osu.Game.Overlays.Volume
text.Text = intValue.ToString(CultureInfo.CurrentCulture);
}
volumeCircle.Current.Value = displayVolume * 0.75f;
volumeCircleGlow.Current.Value = displayVolume * 0.75f;
volumeCircle.Progress = displayVolume * 0.75f;
volumeCircleGlow.Progress = displayVolume * 0.75f;
if (intVolumeChanged && IsLoaded)
Scheduler.AddOnce(playTickSound);

View File

@ -11,3 +11,6 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]
[assembly: InternalsVisibleTo("osu.Game.Tests.iOS")]
[assembly: InternalsVisibleTo("osu.Game.Tests.Android")]
// intended for Moq usage
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Scoring
{
/// <summary>
@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public partial class AccumulatingHealthProcessor : HealthProcessor
{
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth;
private readonly double requiredHealth;

View File

@ -142,6 +142,14 @@ namespace osu.Game.Rulesets.Scoring
}
}
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring
public event Func<bool>? Failed;
/// <summary>
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
/// Additional conditions on top of <see cref="CheckDefaultFailCondition"/> that cause a failing state.
/// </summary>
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
@ -69,9 +69,10 @@ namespace osu.Game.Rulesets.Scoring
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
/// <summary>
/// The default conditions for failing.
/// Checks whether the default conditions for failing are met.
/// </summary>
protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <returns><see langword="true"/> if failure should be invoked.</returns>
protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <summary>
/// Whether the current state of <see cref="HealthProcessor"/> or the provided <paramref name="result"/> meets any fail condition.
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The judgement result.</param>
private bool meetsAnyFailCondition(JudgementResult result)
{
if (DefaultFailCondition)
if (CheckDefaultFailCondition(result))
return true;
if (FailConditions != null)

View File

@ -81,6 +81,19 @@ namespace osu.Game.Rulesets.UI
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
private bool allowBackwardsSeeks;
public override bool AllowBackwardsSeeks
{
get => allowBackwardsSeeks;
set
{
allowBackwardsSeeks = value;
if (frameStabilityContainer != null)
frameStabilityContainer.AllowBackwardsSeeks = value;
}
}
private bool frameStablePlayback = true;
internal override bool FrameStablePlayback
@ -178,6 +191,7 @@ namespace osu.Game.Rulesets.UI
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
{
FrameStablePlayback = FrameStablePlayback,
AllowBackwardsSeeks = AllowBackwardsSeeks,
Children = new Drawable[]
{
FrameStableComponents,
@ -463,6 +477,12 @@ namespace osu.Game.Rulesets.UI
/// </summary>
internal abstract bool FrameStablePlayback { get; set; }
/// <summary>
/// When a replay is not attached, we usually block any backwards seeks.
/// This will bypass the check. Should only be used for tests.
/// </summary>
public abstract bool AllowBackwardsSeeks { get; set; }
/// <summary>
/// The mods which are to be applied.
/// </summary>

View File

@ -3,14 +3,19 @@
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.UI
{
@ -24,6 +29,9 @@ namespace osu.Game.Rulesets.UI
{
public ReplayInputHandler? ReplayInputHandler { get; set; }
public bool AllowBackwardsSeeks { get; set; }
private double? lastBackwardsSeekLogTime;
/// <summary>
/// The number of CPU milliseconds to spend at most during seek catch-up.
/// </summary>
@ -150,6 +158,29 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.NotValid;
}
// This is a hotfix for https://github.com/ppy/osu/issues/26879 while we figure how the hell time is seeking
// backwards by 11,850 ms for some users during gameplay.
//
// It basically says that "while we're running in frame stable mode, and don't have a replay attached,
// time should never go backwards". If it does, we stop running gameplay until it returns to normal.
if (!hasReplayAttached && FrameStablePlayback && proposedTime > referenceClock.CurrentTime && !AllowBackwardsSeeks)
{
if (lastBackwardsSeekLogTime == null || Math.Abs(Clock.CurrentTime - lastBackwardsSeekLogTime.Value) > 1000)
{
lastBackwardsSeekLogTime = Clock.CurrentTime;
string loggableContent = $"Denying backwards seek during gameplay (reference: {referenceClock.CurrentTime:N2} stable: {proposedTime:N2})";
if (parentGameplayClock is GameplayClockContainer gcc)
loggableContent += $"\n{gcc.ChildrenOfType<FramedBeatmapClock>().Single().GetSnapshot()}";
Logger.Error(new SentryOnlyDiagnosticsException("backwards seek"), loggableContent);
}
state = PlaybackState.NotValid;
return;
}
// if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously.
// this avoids spurious flips in direction from -1 to 1 during rewinds.
if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
@ -158,7 +189,7 @@ namespace osu.Game.Rulesets.UI
double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime);
isCatchingUp.Value = timeBehind > 200;
waitingOnFrames.Value = state == PlaybackState.NotValid;
waitingOnFrames.Value = hasReplayAttached && state == PlaybackState.NotValid;
manualClock.CurrentTime = proposedTime;
manualClock.Rate = Math.Abs(referenceClock.Rate) * direction;

View File

@ -42,8 +42,8 @@ namespace osu.Game.Scoring.Legacy
{
OnlineID = score.OnlineID,
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
ClientVersion = score.ClientVersion,
};
}

View File

@ -198,10 +198,25 @@ namespace osu.Game.Scoring.Legacy
}
}
public static int? GetCountMiss(this ScoreInfo scoreInfo) =>
getCount(scoreInfo, HitResult.Miss);
public static int? GetCountMiss(this ScoreInfo scoreInfo)
{
switch (scoreInfo.Ruleset.OnlineID)
{
case 0:
case 1:
case 3:
return getCount(scoreInfo, HitResult.Miss);
case 2:
return (getCount(scoreInfo, HitResult.Miss) ?? 0) + (getCount(scoreInfo, HitResult.LargeTickMiss) ?? 0);
}
return null;
}
public static void SetCountMiss(this ScoreInfo scoreInfo, int value) =>
// this does not match the implementation of `GetCountMiss()` for catch,
// but we physically cannot recover that data anymore at this point.
scoreInfo.Statistics[HitResult.Miss] = value;
private static int? getCount(ScoreInfo scoreInfo, HitResult result)

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -53,7 +52,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
// for changes. ControlPointInfo needs a refactor to make this flow better, but it should do for now.
Scheduler.AddDelayed(() =>
{
var next = beatmap.ControlPointInfo.EffectPoints.FirstOrDefault(c => c.Time > effect.Time);
EffectControlPoint? next = null;
for (int i = 0; i < beatmap.ControlPointInfo.EffectPoints.Count; i++)
{
var point = beatmap.ControlPointInfo.EffectPoints[i];
if (point.Time > effect.Time)
{
next = point;
break;
}
}
if (!ReferenceEquals(nextControlPoint, next))
{

View File

@ -140,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Colour = this.baseColour = baseColour;
Current.Value = 1;
Progress = 1;
}
protected override void Update()

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor;
private readonly EditorState editorState;
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo);
[Resolved]
private MusicController musicController { get; set; } = null!;

View File

@ -366,7 +366,7 @@ namespace osu.Game.Screens.Edit.Timing
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Progress = 1f / light_count - angular_light_gap,
Colour = colourProvider.Background2,
},
fillContent = new Container
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Edit.Timing
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Progress = 1f / light_count - angular_light_gap,
Blending = BlendingParameters.Additive
},
// Please do not try and make sense of this.
@ -388,7 +388,7 @@ namespace osu.Game.Screens.Edit.Timing
Glow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - 0.01f },
Progress = 1f / light_count - 0.01f,
Blending = BlendingParameters.Additive
}.WithEffect(new GlowEffect
{

View File

@ -14,7 +14,6 @@ using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osuTK;
using osuTK.Graphics;
@ -209,13 +208,13 @@ namespace osu.Game.Screens.Menu
if (audioData[i] < amplitude_dead_zone)
continue;
float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotation = float.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotationCos = MathF.Cos(rotation);
float rotationSin = MathF.Sin(rotation);
// taking the cos and sin to the 0..1 range
var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size;
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
// The distance between the position and the sides of the bar.
var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2);
// The distance between the bottom side of the bar and the top side.

View File

@ -122,8 +122,17 @@ namespace osu.Game.Screens.Play
StopGameplayClock();
}
protected virtual void StartGameplayClock() => GameplayClock.Start();
protected virtual void StopGameplayClock() => GameplayClock.Stop();
protected virtual void StartGameplayClock()
{
Logger.Log($"{nameof(GameplayClockContainer)} started via call to {nameof(StartGameplayClock)}");
GameplayClock.Start();
}
protected virtual void StopGameplayClock()
{
Logger.Log($"{nameof(GameplayClockContainer)} stopped via call to {nameof(StopGameplayClock)}");
GameplayClock.Stop();
}
/// <summary>
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Storyboards;
namespace osu.Game.Screens.Play
{
@ -40,6 +41,11 @@ namespace osu.Game.Screens.Play
public readonly ScoreProcessor ScoreProcessor;
/// <summary>
/// The storyboard associated with the beatmap.
/// </summary>
public readonly Storyboard Storyboard;
/// <summary>
/// Whether gameplay completed without the user failing.
/// </summary>
@ -62,7 +68,7 @@ namespace osu.Game.Screens.Play
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null)
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null, Storyboard? storyboard = null)
{
Beatmap = beatmap;
Ruleset = ruleset;
@ -76,6 +82,7 @@ namespace osu.Game.Screens.Play
};
Mods = mods ?? Array.Empty<Mod>();
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
Storyboard = storyboard ?? new Storyboard();
}
/// <summary>

View File

@ -107,8 +107,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
JudgementSpacing.BindValueChanged(_ => updateMetrics(), true);
}
private readonly DrawablePool<HitErrorShape> judgementLinePool = new DrawablePool<HitErrorShape>(50);
public void Push(HitErrorShape shape)
{
Add(shape);

View File

@ -198,9 +198,14 @@ namespace osu.Game.Screens.Play.HUD
bind();
}
protected override void Update()
{
base.Update();
circularProgress.Progress = Progress.Value;
}
private void bind()
{
((IBindable<double>)circularProgress.Current).BindTo(Progress);
Progress.ValueChanged += progress =>
{
icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f);

View File

@ -255,7 +255,7 @@ namespace osu.Game.Screens.Play
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor));
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, Beatmap.Value.Storyboard));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
@ -397,7 +397,7 @@ namespace osu.Game.Screens.Play
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
{
@ -456,7 +456,7 @@ namespace osu.Game.Screens.Play
{
RequestSkip = performUserRequestedSkip
},
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
skipOutroOverlay = new SkipOverlay(GameplayState.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => progressToResults(false),
Alpha = 0
@ -1088,7 +1088,7 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
storyboardReplacesBackground.Value = GameplayState.Storyboard.ReplacesBackground && GameplayState.Storyboard.HasDrawable;
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(this);

View File

@ -31,6 +31,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
public partial class AccuracyCircle : CompositeDrawable
{
/// <summary>
/// The total duration of the animation.
/// </summary>
public const double TOTAL_DURATION = APPEAR_DURATION + ACCURACY_TRANSFORM_DELAY + ACCURACY_TRANSFORM_DURATION;
/// <summary>
/// Duration for the transforms causing this component to appear.
/// </summary>
@ -147,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Colour = OsuColour.Gray(47),
Alpha = 0.5f,
InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle
Current = { Value = 1 },
Progress = 1,
},
accuracyCircle = new CircularProgress
{
@ -268,7 +273,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
targetAccuracy -= visual_alignment_offset;
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
accuracyCircle.ProgressTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
if (withFlair)
{
@ -359,7 +364,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
.FadeOut(800, Easing.Out);
accuracyCircle
.FillTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
.ProgressTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
badges.Single(b => b.Rank == getRank(ScoreRank.S))
.FadeOut(70, Easing.OutQuint);

View File

@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
public double RevealProgress
{
set => Current.Value = Math.Clamp(value, startProgress, endProgress) - startProgress;
set => Progress = Math.Clamp(value, startProgress, endProgress) - startProgress;
}
private readonly double startProgress;

View File

@ -25,8 +25,10 @@ using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
@ -41,6 +43,8 @@ namespace osu.Game.Screens.Ranking
public override bool? AllowGlobalTrackControl => true;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[CanBeNull]
@ -172,6 +176,10 @@ namespace osu.Game.Screens.Ranking
bool shouldFlair = player != null && !Score.User.IsBot;
ScorePanelList.AddScore(Score, shouldFlair);
// this is mostly for medal display.
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
// to give the results screen enough time to play the animation out before the medals can be shown.
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
}
if (AllowWatchingReplay)

View File

@ -72,14 +72,14 @@ namespace osu.Game.Skinning
circularProgress.Scale = new Vector2(-1, 1);
circularProgress.Anchor = Anchor.TopRight;
circularProgress.Colour = new Colour4(199, 255, 47, 153);
circularProgress.Current.Value = 1 - progress;
circularProgress.Progress = 1 - progress;
}
else
{
circularProgress.Scale = new Vector2(1);
circularProgress.Anchor = Anchor.TopLeft;
circularProgress.Colour = new Colour4(255, 255, 255, 153);
circularProgress.Current.Value = progress;
circularProgress.Progress = progress;
}
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Skinning
private TextureUpload convertToGrayscale(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// stable uses `0.299 * r + 0.587 * g + 0.114 * b`
// (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Skinning
if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();

View File

@ -70,10 +70,20 @@ namespace osu.Game.Tests.Visual
AddStep($"Load player for {CreatePlayerRuleset().Description}", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
if (AllowBackwardsSeeks)
{
AddStep("allow backwards seeking", () =>
{
Player.DrawableRuleset.AllowBackwardsSeeks = AllowBackwardsSeeks;
});
}
}
protected virtual bool AllowFail => false;
protected virtual bool AllowBackwardsSeeks => false;
protected virtual bool Autoplay => false;
protected void LoadPlayer() => LoadPlayer(Array.Empty<Mod>());
@ -126,6 +136,6 @@ namespace osu.Game.Tests.Visual
protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset();
protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false);
protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false, AllowBackwardsSeeks);
}
}

View File

@ -119,10 +119,10 @@ namespace osu.Game.Users
}
[MessagePackObject]
public class TestingBeatmap : InGame
public class TestingBeatmap : EditingBeatmap
{
public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
: base(beatmapInfo, ruleset)
public TestingBeatmap(IBeatmapInfo beatmapInfo)
: base(beatmapInfo)
{
}
@ -151,7 +151,11 @@ namespace osu.Game.Users
public EditingBeatmap() { }
public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap";
public override string GetDetails(bool hideIdentifiableInformation = false) => BeatmapDisplayTitle;
public override string GetDetails(bool hideIdentifiableInformation = false) => hideIdentifiableInformation
// For now let's assume that showing the beatmap a user is editing could reveal unwanted information.
? string.Empty
: BeatmapDisplayTitle;
}
[MessagePackObject]

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@ -28,8 +27,8 @@ namespace osu.Game.Utils
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X = point.X * MathF.Cos(float.DegreesToRadians(angle)) + point.Y * MathF.Sin(float.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(float.DegreesToRadians(angle)) + point.Y * MathF.Cos(float.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;

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