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:
commit
13f7480d79
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@ -173,13 +174,16 @@ namespace osu.Desktop
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
|
17
osu.Desktop/Windows/Icons.cs
Normal file
17
osu.Desktop/Windows/Icons.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
public static class Icons
|
||||
{
|
||||
/// <summary>
|
||||
/// Fully qualified path to the directory that contains icons (in the installation folder).
|
||||
/// </summary>
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
}
|
||||
}
|
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
292
osu.Desktop/Windows/WindowsAssociationManager.cs
Normal file
@ -0,0 +1,292 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Desktop.Windows
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
|
||||
/// </summary>
|
||||
private const string default_icon = @"DefaultIcon";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the command line that the shell invokes.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
|
||||
/// </summary>
|
||||
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
|
||||
|
||||
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
|
||||
|
||||
/// <summary>
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
{
|
||||
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
|
||||
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to update file and URI association descriptions.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void UninstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Uninstall();
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, @"Failed to uninstall file and URI associations.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Installs or updates associations.
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s)
|
||||
{
|
||||
if (localisation == null)
|
||||
return s.ToString();
|
||||
|
||||
var b = localisation.GetLocalisedBindableString(s);
|
||||
b.UnbindAll();
|
||||
return b.Value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
private enum EventId
|
||||
{
|
||||
/// <summary>
|
||||
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
|
||||
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
|
||||
/// </summary>
|
||||
SHCNE_ASSOCCHANGED = 0x08000000
|
||||
}
|
||||
|
||||
private enum Flags : uint
|
||||
{
|
||||
SHCNF_IDLIST = 0x0000
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
|
||||
/// </summary>
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
|
||||
/// <summary>
|
||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,4 +31,7 @@
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Windows Icons">
|
||||
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -20,6 +20,7 @@
|
||||
<file src="**.dll" target="lib\net45\"/>
|
||||
<file src="**.config" target="lib\net45\"/>
|
||||
<file src="**.json" target="lib\net45\"/>
|
||||
<file src="**.ico" target="lib\net45\"/>
|
||||
<file src="icon.png" target=""/>
|
||||
</files>
|
||||
</package>
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
|
||||
[TestCase("112643")]
|
||||
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
58
osu.Game.Rulesets.Catch.Tests/CatchHealthProcessorTest.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class CatchHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Fruit(), 0.01, true],
|
||||
[new Droplet(), 0.01, true],
|
||||
[new TinyDroplet(), 0, false],
|
||||
[new Banana(), 0, false],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new CatchHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new CatchBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,210 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 65316
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
DistanceSpacing: 1.4
|
||||
BeatDivisor: 4
|
||||
GridSize: 8
|
||||
TimelineZoom: 1.4
|
||||
|
||||
[Metadata]
|
||||
Title:Nanairo Symphony -TV Size-
|
||||
TitleUnicode:七色シンフォニー -TV Size-
|
||||
Artist:Coalamode.
|
||||
ArtistUnicode:コアラモード.
|
||||
Creator:Ascendance
|
||||
Version:Aru's Cup
|
||||
Source:四月は君の嘘
|
||||
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
|
||||
BeatmapID:1041052
|
||||
BeatmapSetID:488149
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:3
|
||||
CircleSize:2.5
|
||||
OverallDifficulty:6
|
||||
ApproachRate:6
|
||||
SliderMultiplier:1.02
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
Video,500,"forty.avi"
|
||||
0,0,"cropped-1366-768-647733.jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
1155,387.096774193548,4,2,1,50,1,0
|
||||
15284,-100,4,2,1,60,0,0
|
||||
16638,-100,4,2,1,50,0,0
|
||||
41413,-100,4,2,1,60,0,0
|
||||
59993,-100,4,2,1,65,0,0
|
||||
66187,-100,4,2,1,70,0,1
|
||||
87284,-100,4,2,1,60,0,1
|
||||
87864,-100,4,2,1,70,0,0
|
||||
87961,-100,4,2,1,50,0,0
|
||||
88638,-100,4,2,1,30,0,0
|
||||
89413,-100,4,2,1,10,0,0
|
||||
89800,-100,4,2,1,5,0,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1 : 255,128,64
|
||||
Combo2 : 0,128,255
|
||||
Combo3 : 255,128,192
|
||||
Combo4 : 0,128,192
|
||||
|
||||
[HitObjects]
|
||||
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
160,160,2122,1,0,0:0:0:0:
|
||||
272,160,2509,1,2,0:0:0:0:
|
||||
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
384,96,4058,1,2,0:0:0:0:
|
||||
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,64,5800,1,2,0:0:0:0:
|
||||
240,64,5993,1,2,0:0:0:0:
|
||||
288,64,6187,1,2,0:0:0:0:
|
||||
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
|
||||
488,160,8122,2,0,L|376:160,1,102
|
||||
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
400,288,10058,1,0,0:0:0:0:
|
||||
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
|
||||
400,288,11606,1,0,0:0:0:0:
|
||||
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
0,288,13154,1,0,0:0:0:0:
|
||||
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
|
||||
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
|
||||
312,208,15864,1,6,0:0:0:0:
|
||||
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
432,176,20896,1,0,0:0:0:0:
|
||||
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
328,176,22445,1,0,0:0:0:0:
|
||||
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
224,176,23993,1,0,0:0:0:0:
|
||||
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
416,176,25541,1,0,0:0:0:0:
|
||||
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
480,112,27090,1,0,0:0:0:0:
|
||||
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
432,112,28058,1,2,0:0:0:0:
|
||||
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
|
||||
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
272,272,30574,2,0,L|374:272,1,102
|
||||
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
|
||||
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
200,368,32703,1,2,0:0:0:0:
|
||||
376,368,33284,1,0,0:0:0:0:
|
||||
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
|
||||
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
280,16,36380,1,2,0:0:0:0:
|
||||
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
176,224,37928,1,2,0:0:0:0:
|
||||
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
144,264,38896,1,2,0:0:0:0:
|
||||
280,336,39477,2,0,L|336:336,1,51
|
||||
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
|
||||
352,232,40445,1,4,0:1:0:0:
|
||||
160,224,41025,1,8,0:3:0:0:
|
||||
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
400,40,41993,1,0,0:0:0:0:
|
||||
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
|
||||
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
56,136,45284,1,2,0:0:0:0:
|
||||
160,144,45671,1,8,0:0:0:0:
|
||||
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
|
||||
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
144,136,49154,1,2,0:0:0:0:
|
||||
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
|
||||
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
|
||||
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
400,208,51284,1,0,0:0:0:0:
|
||||
352,240,51477,2,0,L|240:240,1,102
|
||||
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
|
||||
88,264,53025,1,2,0:0:0:0:
|
||||
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
|
||||
432,120,54574,1,2,0:0:0:0:
|
||||
328,128,54961,1,8,0:0:0:0:
|
||||
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
224,184,56509,1,8,0:3:0:0:
|
||||
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
|
||||
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
224,272,58445,5,2,0:0:0:0:
|
||||
320,296,58832,1,0,0:0:0:0:
|
||||
224,328,59219,1,2,0:0:0:0:
|
||||
120,328,59606,1,8,0:3:0:0:
|
||||
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
|
||||
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
|
||||
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
160,112,64638,1,8,0:0:0:0:
|
||||
208,128,64832,1,8,0:0:0:0:
|
||||
256,144,65025,1,8,0:0:0:0:
|
||||
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
176,208,68316,1,0,0:0:0:0:
|
||||
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
|
||||
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
|
||||
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
|
||||
208,248,77800,1,8,0:0:0:0:
|
||||
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
|
||||
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
96,72,80703,1,0,0:0:0:0:
|
||||
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
|
||||
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
|
||||
208,272,82445,1,10,0:0:0:0:
|
||||
312,272,82832,1,8,0:0:0:0:
|
||||
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
|
||||
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
|
||||
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
|
||||
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
|
||||
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
|
||||
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
|
||||
168,96,87864,1,4,0:0:0:0:
|
||||
256,192,88251,12,0,89800,0:0:0:0:
|
@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
float offsetPosition = hitObject.OriginalX;
|
||||
double startTime = hitObject.StartTime;
|
||||
|
||||
if (lastPosition == null)
|
||||
if (lastPosition == null ||
|
||||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
|
||||
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
|
||||
// todo: should be revisited and corrected later probably.
|
||||
lastPosition == 0)
|
||||
{
|
||||
lastPosition = offsetPosition;
|
||||
lastStartTime = startTime;
|
||||
|
@ -2,22 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
private int fruitsHit;
|
||||
private int ticksHit;
|
||||
private int tinyTicksHit;
|
||||
private int tinyTicksMissed;
|
||||
private int misses;
|
||||
private int num300;
|
||||
private int num100;
|
||||
private int num50;
|
||||
private int numKatu;
|
||||
private int numMiss;
|
||||
|
||||
public CatchPerformanceCalculator()
|
||||
: base(new CatchRuleset())
|
||||
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
var catchAttributes = (CatchDifficultyAttributes)attributes;
|
||||
|
||||
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
|
||||
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
|
||||
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
num300 = score.GetCount300() ?? 0; // HitResult.Great
|
||||
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
|
||||
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
|
||||
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
|
||||
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
|
||||
|
||||
// We are heavily relying on aim in catch the beat
|
||||
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
|
||||
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||
value *= lengthBonus;
|
||||
|
||||
value *= Math.Pow(0.97, misses);
|
||||
value *= Math.Pow(0.97, numMiss);
|
||||
|
||||
// Combo scaling
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
|
||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
|
||||
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
|
||||
private int totalComboHits() => misses + ticksHit + fruitsHit;
|
||||
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
|
||||
private int totalSuccessfulHits() => num50 + num100 + num300;
|
||||
private int totalComboHits() => numMiss + num100 + num300;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override bool CheckDefaultFailCondition(JudgementResult result)
|
||||
{
|
||||
// matches stable.
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
|
||||
// the above early-return skips the failure check at the end of the same method:
|
||||
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
|
||||
// making it impossible to fail on a tiny droplet regardless of result.
|
||||
if (result.Type == HitResult.SmallTickMiss)
|
||||
return false;
|
||||
|
||||
return base.CheckDefaultFailCondition(result);
|
||||
}
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new Note(), 0.01, true],
|
||||
[new HeadNote(), 0.01, true],
|
||||
[new TailNote(), 0.01, true],
|
||||
[new HoldNoteBody(), 0, true], // hold note break
|
||||
[new HoldNote(), 0, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new ManiaHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,16 +34,21 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("setup hierarchy", () => Child = new Container
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
Child = new Container
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
Clock = new FramedClock(clock = new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
66
osu.Game.Rulesets.Osu.Tests/OsuHealthProcessorTest.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class OsuHealthProcessorTest
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, starting HP, fail expected after miss
|
||||
[new HitCircle(), 0.01, true],
|
||||
[new SliderHeadCircle(), 0.01, true],
|
||||
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SliderTick(), 0.01, true],
|
||||
[new SliderRepeat(new Slider()), 0.01, true],
|
||||
[new SliderTailCircle(new Slider()), 0, true],
|
||||
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new Slider(), 0, true],
|
||||
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
|
||||
[new SpinnerTick(), 0, false],
|
||||
[new SpinnerBonusTick(), 0, false],
|
||||
[new Spinner(), 0.01, true],
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
|
||||
{
|
||||
var healthProcessor = new OsuHealthProcessor(0);
|
||||
healthProcessor.ApplyBeatmap(new OsuBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
healthProcessor.Health.Value = startingHealth;
|
||||
|
||||
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
|
||||
// some spinners may not complete due to very minor decimal loss during calculation
|
||||
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
|
||||
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4.White.Opacity(0.25f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = 90 - arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
background.Alpha = spinner.Progress >= 1 ? 0 : 1;
|
||||
|
||||
fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(90 - fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(90 - fill.Progress * 180);
|
||||
}
|
||||
|
||||
private partial class ProgressFill : CircularProgress
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Current = { Value = arc_fill },
|
||||
Progress = arc_fill,
|
||||
Rotation = -arc_fill * 180,
|
||||
InnerRadius = arc_radius,
|
||||
RoundedCaps = true,
|
||||
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
base.Update();
|
||||
|
||||
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
|
||||
|
||||
fill.Rotation = (float)(-fill.Current.Value * 180);
|
||||
fill.Rotation = (float)(-fill.Progress * 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
|
||||
//
|
||||
// We also need to apply the anti-clockwise rotation.
|
||||
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
|
||||
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
|
||||
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
|
||||
|
||||
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissHitAndHitSwell()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Swell { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[1].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
[
|
||||
// hitobject, fail expected after miss
|
||||
[new Hit(), true],
|
||||
[new Hit.StrongNestedHit(new Hit()), false],
|
||||
[new DrumRollTick(new DrumRoll()), false],
|
||||
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
|
||||
[new DrumRoll(), false],
|
||||
[new SwellTick(), false],
|
||||
[new Swell(), false]
|
||||
];
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MinResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
|
||||
{
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(new TaikoBeatmap
|
||||
{
|
||||
HitObjects = { hitObject }
|
||||
});
|
||||
|
||||
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
|
||||
result.Type = result.Judgement.MaxResult;
|
||||
healthProcessor.ApplyResult(result);
|
||||
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
public partial class TestSceneTaikoPlayerScroller : LegacySkinPlayerTestScene
|
||||
{
|
||||
private Storyboard? currentStoryboard;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[Test]
|
||||
public void TestForegroundSpritesHidesScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Foreground").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller not present", () => !this.ChildrenOfType<LegacyTaikoScroller>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlaySpritesKeepsScroller()
|
||||
{
|
||||
AddStep("load storyboard", () =>
|
||||
{
|
||||
currentStoryboard = new Storyboard();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
currentStoryboard.GetLayer("Overlay").Add(new StoryboardSprite($"test{i}", Anchor.Centre, Vector2.Zero));
|
||||
});
|
||||
|
||||
CreateTest();
|
||||
AddAssert("taiko scroller present", () => this.ChildrenOfType<LegacyTaikoScroller>().Single().IsPresent);
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> base.CreateWorkingBeatmap(beatmap, currentStoryboard ?? storyboard);
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -22,7 +24,9 @@ using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
@ -39,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinnableDrawable scroller;
|
||||
|
||||
public DrawableTaikoRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
@ -48,16 +53,24 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
VisualisationMethod = ScrollVisualisationMethod.Overlapping;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] GameplayState gameplayState)
|
||||
{
|
||||
new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar));
|
||||
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
var spriteElements = gameplayState?.Storyboard.Layers.Where(l => l.Name != @"Overlay")
|
||||
.SelectMany(l => l.Elements)
|
||||
.OfType<StoryboardSprite>()
|
||||
.DistinctBy(e => e.Path) ?? Enumerable.Empty<StoryboardSprite>();
|
||||
|
||||
if (spriteElements.Count() < 10)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Scroller), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
}
|
||||
|
||||
KeyBindingInputManager.Add(new DrumTouchInputArea());
|
||||
}
|
||||
@ -76,7 +89,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
var playfieldScreen = Playfield.ScreenSpaceDrawQuad;
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
|
||||
if (scroller != null)
|
||||
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
|
||||
}
|
||||
|
||||
public MultiplierControlPoint ControlPointAt(double time)
|
||||
|
@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
public class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private readonly int beatmapVersion;
|
||||
|
||||
|
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
55
osu.Game.Tests/Beatmaps/Formats/LegacyScoreEncoderTest.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
public class LegacyScoreEncoderTest
|
||||
{
|
||||
[TestCase(1, 3)]
|
||||
[TestCase(1, 0)]
|
||||
[TestCase(0, 3)]
|
||||
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
|
||||
{
|
||||
var ruleset = new CatchRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
scoreInfo.Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 50,
|
||||
[HitResult.LargeTickHit] = 5,
|
||||
[HitResult.Miss] = missCount,
|
||||
[HitResult.LargeTickMiss] = largeTickMissCount
|
||||
};
|
||||
var score = new Score { ScoreInfo = scoreInfo };
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||
encoder.Encode(encodeStream);
|
||||
|
||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||
|
||||
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
|
||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||
return decodedAfterEncode;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override bool AllowFail => false;
|
||||
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
|
@ -130,8 +130,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
|
||||
{
|
||||
mainContainer.Child = new FrameStabilityContainer(gameplayStartTime)
|
||||
.WithChild(consumer = new ClockConsumingChild()));
|
||||
{
|
||||
AllowBackwardsSeeks = true,
|
||||
}.WithChild(consumer = new ClockConsumingChild());
|
||||
});
|
||||
|
||||
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
|
||||
|
||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySamplePlayback : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
[Test]
|
||||
public void TestAllSamplesStopDuringSeek()
|
||||
{
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
|
||||
{
|
||||
protected override bool AllowBackwardsSeeks => true;
|
||||
|
||||
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
|
@ -288,6 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public override Container FrameStableComponents { get; }
|
||||
public override IFrameStableClock FrameStableClock { get; }
|
||||
internal override bool FrameStablePlayback { get; set; }
|
||||
public override bool AllowBackwardsSeeks { get; set; }
|
||||
public override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
public override double GameplayStartTime { get; }
|
||||
|
@ -1,26 +1,109 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneMedalOverlay : OsuTestScene
|
||||
public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
|
||||
{
|
||||
public TestSceneMedalOverlay()
|
||||
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
private MedalOverlay overlay = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep(@"display", () =>
|
||||
var overlayManagerMock = new Mock<IOverlayManager>();
|
||||
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
|
||||
|
||||
AddStep("create overlay", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
LoadComponentAsync(new MedalOverlay(new Medal
|
||||
{
|
||||
Name = @"Animations",
|
||||
InternalName = @"all-intro-doubletime",
|
||||
Description = @"More complex than you think.",
|
||||
}), Add);
|
||||
Child = overlay = new MedalOverlay(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies =
|
||||
[
|
||||
(typeof(IOverlayManager), overlayManagerMock.Object)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicAward()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
|
||||
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleMedalsInQuickSuccession()
|
||||
{
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "S-Ranker",
|
||||
Description = "Accuracy is really underrated.",
|
||||
Slug = @"all-secret-rank-s"
|
||||
});
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "500 Combo",
|
||||
Description = "500 big ones! You're moving up in the world!",
|
||||
Slug = @"osu-combo-500"
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
|
||||
{
|
||||
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
|
||||
awardMedal(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
});
|
||||
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
|
||||
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
|
||||
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(unlock)
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private bool gameplayClockAlwaysGoingForward = true;
|
||||
private double lastForwardCheckTime;
|
||||
|
||||
public TestScenePause()
|
||||
{
|
||||
base.Content.Add(content = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both });
|
||||
@ -67,12 +71,20 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
confirmPausedWithNoOverlay();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestForwardPlaybackGuarantee()
|
||||
{
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddUntilStep("wait for forward playback", () => Player.GameplayClockContainer.CurrentTime > 1000);
|
||||
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||
|
||||
checkForwardPlayback();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseWithLargeOffset()
|
||||
{
|
||||
double lastStopTime;
|
||||
bool alwaysGoingForward = true;
|
||||
|
||||
AddStep("force large offset", () =>
|
||||
{
|
||||
var offset = (BindableDouble)LocalConfig.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
@ -82,25 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
offset.Value = -5000;
|
||||
});
|
||||
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastStopTime = double.MinValue;
|
||||
alwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
|
||||
|
||||
double currentTime = masterClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastStopTime;
|
||||
|
||||
alwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
hookForwardPlaybackCheck();
|
||||
|
||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||
|
||||
@ -108,11 +102,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
resumeAndConfirm();
|
||||
|
||||
AddAssert("time didn't go too far backwards", () => alwaysGoingForward);
|
||||
checkForwardPlayback();
|
||||
|
||||
AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||
}
|
||||
|
||||
private void checkForwardPlayback() => AddAssert("time didn't go too far backwards", () => gameplayClockAlwaysGoingForward);
|
||||
|
||||
private void hookForwardPlaybackCheck()
|
||||
{
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastForwardCheckTime = double.MinValue;
|
||||
gameplayClockAlwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
var frameStableClock = Player.ChildrenOfType<FrameStabilityContainer>().Single().Clock;
|
||||
|
||||
double currentTime = frameStableClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= lastForwardCheckTime;
|
||||
lastForwardCheckTime = currentTime;
|
||||
|
||||
gameplayClockAlwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Went too far backwards (last stop: {lastForwardCheckTime:N1} current: {currentTime:N1})");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseResume()
|
||||
{
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private TextureUpload upscale(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
@ -269,6 +269,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo));
|
||||
drawableRuleset.FrameStablePlayback = true;
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
drawableRuleset.PoolSize = poolSize;
|
||||
|
||||
Child = new Container
|
||||
|
@ -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;
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowMedalAtResults()
|
||||
{
|
||||
playToResults();
|
||||
|
||||
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
|
||||
{
|
||||
Event = @"new",
|
||||
Data = JObject.FromObject(new NewPrivateNotificationEvent
|
||||
{
|
||||
Name = @"user_achievement_unlock",
|
||||
Details = JObject.FromObject(new UserAchievementUnlock
|
||||
{
|
||||
Title = "Time And A Half",
|
||||
Description = "Having a right ol' time. One and a half of them, almost.",
|
||||
Slug = @"all-intro-doubletime"
|
||||
})
|
||||
})
|
||||
}));
|
||||
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryFromResults()
|
||||
{
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Users;
|
||||
@ -142,7 +141,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
|
||||
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
|
||||
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(new BeatmapInfo()));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo(), new OsuRuleset().RulesetInfo));
|
||||
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(new BeatmapInfo()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
39
osu.Game.Tests/Visual/Ranking/TestSceneDrawableRank.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneDrawableRank : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestAllRanks()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer<DrawableRank>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(10),
|
||||
ChildrenEnumerable = Enum.GetValues<ScoreRank>().OrderBy(v => v).Select(rank => new DrawableRank(rank)
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
Size = new Vector2(50, 25),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
Size size = image.Size();
|
||||
Size size = image.Size;
|
||||
|
||||
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
|
||||
// Also assume that all backgrounds are wider than they are tall, so the
|
||||
|
@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
Dimmed.BindValueChanged(_ => updateState());
|
||||
|
||||
playButton.Playing.BindValueChanged(_ => updateState(), true);
|
||||
((IBindable<double>)progress.Current).BindTo(playButton.Progress);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
progress.Progress = playButton.Progress.Value;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
bool shouldDim = Dimmed.Value || playButton.Playing.Value;
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Storyboards;
|
||||
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
float startValue = Parsing.ParseFloat(split[4]);
|
||||
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
|
||||
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
private readonly DecouplingFramedClock decoupledTrack;
|
||||
private readonly InterpolatingFramedClock interpolatedTrack;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
|
||||
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
|
||||
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||
|
||||
if (applyOffsets)
|
||||
{
|
||||
@ -190,5 +191,28 @@ namespace osu.Game.Beatmaps
|
||||
base.Dispose(isDisposing);
|
||||
beatmapOffsetSubscription?.Dispose();
|
||||
}
|
||||
|
||||
public string GetSnapshot()
|
||||
{
|
||||
return
|
||||
$"originalSource: {output(Source)}\n" +
|
||||
$"userGlobalOffsetClock: {output(userGlobalOffsetClock)}\n" +
|
||||
$"platformOffsetClock: {output(platformOffsetClock)}\n" +
|
||||
$"userBeatmapOffsetClock: {output(userBeatmapOffsetClock)}\n" +
|
||||
$"interpolatedTrack: {output(interpolatedTrack)}\n" +
|
||||
$"decoupledTrack: {output(decoupledTrack)}\n" +
|
||||
$"finalClockSource: {output(finalClockSource)}\n";
|
||||
|
||||
string output(IClock? clock)
|
||||
{
|
||||
if (clock == null)
|
||||
return "null";
|
||||
|
||||
if (clock is IFrameBasedClock framed)
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate} elapsed: {framed.ElapsedFrameTime:N2}";
|
||||
|
||||
return $"current: {clock.CurrentTime:N2} running: {clock.IsRunning} rate: {clock.Rate}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -63,8 +63,12 @@ namespace osu.Game.Graphics
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"ff8e5d");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"ff5a5a");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"3f3f3f");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Extensions;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class OsuNumberBox : OsuTextBox
|
||||
{
|
||||
protected override bool AllowIme => false;
|
||||
|
||||
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
|
||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
using (Stream s = entry.OpenEntryStream())
|
||||
s.ReadToFill(owner.Memory.Span);
|
||||
s.ReadExactly(owner.Memory.Span);
|
||||
|
||||
return new MemoryOwnerMemoryStream(owner);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Performance points will not be granted due to active mods."
|
||||
/// </summary>
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"ranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
39
osu.Game/Localisation/WindowsAssociationManagerStrings.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class WindowsAssociationManagerStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.WindowsAssociationManager";
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuBeatmap => new TranslatableString(getKey(@"osu_beatmap"), @"osu! Beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Replay"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuReplay => new TranslatableString(getKey(@"osu_replay"), @"osu! Replay");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Skin"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuSkin => new TranslatableString(getKey(@"osu_skin"), @"osu! Skin");
|
||||
|
||||
/// <summary>
|
||||
/// "osu!"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuProtocol => new TranslatableString(getKey(@"osu_protocol"), @"osu!");
|
||||
|
||||
/// <summary>
|
||||
/// "osu! Multiplayer"
|
||||
/// </summary>
|
||||
public static LocalisableString OsuMultiplayer => new TranslatableString(getKey(@"osu_multiplayer"), @"osu! Multiplayer");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
RulesetID = score.RulesetID,
|
||||
Passed = score.Passed,
|
||||
Mods = score.APIMods,
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Notifications.WebSocket;
|
||||
using osu.Game.Online.Notifications.WebSocket.Events;
|
||||
using osu.Game.Online.Notifications.WebSocket.Requests;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
|
@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards
|
||||
case ScoreRank.C:
|
||||
return Color4Extensions.FromHex(@"473625");
|
||||
|
||||
default:
|
||||
case ScoreRank.D:
|
||||
return Color4Extensions.FromHex(@"512525");
|
||||
|
||||
case ScoreRank.F:
|
||||
default:
|
||||
return Color4Extensions.FromHex(@"CC3333");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message sent from the server when new messages arrive.
|
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php
|
||||
/// </summary>
|
||||
public class NewPrivateNotificationEvent
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public ulong ID { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("created_at")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("object_type")]
|
||||
public string ObjectType { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("object_id")]
|
||||
public ulong ObjectId { get; set; }
|
||||
|
||||
[JsonProperty("source_user_id")]
|
||||
public uint SourceUserID { get; set; }
|
||||
|
||||
[JsonProperty("is_read")]
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
[JsonProperty("details")]
|
||||
public JObject? Details { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php
|
||||
/// </summary>
|
||||
public class UserAchievementUnlock
|
||||
{
|
||||
[JsonProperty("achievement_id")]
|
||||
public uint AchievementId { get; set; }
|
||||
|
||||
[JsonProperty("achievement_mode")]
|
||||
public ushort? AchievementMode { get; set; }
|
||||
|
||||
[JsonProperty("cover_url")]
|
||||
public string CoverUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
public uint UserId { get; set; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client no longer wants to receive chat messages.
|
@ -3,7 +3,7 @@
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.Notifications.WebSocket
|
||||
namespace osu.Game.Online.Notifications.WebSocket.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// A websocket message notifying the server that the client wants to receive chat messages.
|
@ -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)
|
||||
|
312
osu.Game/Overlays/MedalAnimation.cs
Normal file
312
osu.Game/Overlays/MedalAnimation.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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")]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -140,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
Colour = this.baseColour = baseColour;
|
||||
|
||||
Current.Value = 1;
|
||||
Progress = 1;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -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!;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user