diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index 7dcdac7019..0522840e9e 100644 --- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Pippidon new KeyBinding(InputKey.X, PippidonAction.Button2), }; - public override Drawable CreateIcon() => new Sprite - { - Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"), - }; + public override Drawable CreateIcon() => new PippidonRulesetIcon(this); } } diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs new file mode 100644 index 0000000000..ff10233b50 --- /dev/null +++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonRulesetIcon : Sprite + { + private readonly Ruleset ruleset; + + public PippidonRulesetIcon(Ruleset ruleset) + { + this.ruleset = ruleset; + } + + [BackgroundDependencyLoader] + private void load(IRenderer renderer) + { + Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin"); + } + } +} diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs index d4566477db..89246373ee 100644 --- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRuleset.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; @@ -47,10 +45,6 @@ namespace osu.Game.Rulesets.Pippidon new KeyBinding(InputKey.S, PippidonAction.MoveDown), }; - public override Drawable CreateIcon() => new Sprite - { - Margin = new MarginPadding { Top = 3 }, - Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"), - }; + public override Drawable CreateIcon() => new PippidonRulesetIcon(this); } } diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs new file mode 100644 index 0000000000..6b87d93951 --- /dev/null +++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/PippidonRulesetIcon.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Rulesets.Pippidon +{ + public class PippidonRulesetIcon : Sprite + { + private readonly Ruleset ruleset; + + public PippidonRulesetIcon(Ruleset ruleset) + { + this.ruleset = ruleset; + + Margin = new MarginPadding { Top = 3 }; + } + + [BackgroundDependencyLoader] + private void load(IRenderer renderer) + { + Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin"); + } + } +} diff --git a/osu.Android.props b/osu.Android.props index 9aba8e236c..247140ceef 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index d0b6953c30..9cf68d88d9 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Text; using DiscordRPC; @@ -26,15 +24,15 @@ namespace osu.Desktop { private const string client_id = "367827983903490050"; - private DiscordRpcClient client; + private DiscordRpcClient client = null!; [Resolved] - private IBindable ruleset { get; set; } + private IBindable ruleset { get; set; } = null!; - private IBindable user; + private IBindable user = null!; [Resolved] - private IAPIProvider api { get; set; } + private IAPIProvider api { get; set; } = null!; private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); @@ -130,7 +128,7 @@ namespace osu.Desktop presence.Assets.LargeImageText = string.Empty; else { - if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics statistics)) + if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); else presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); @@ -164,7 +162,7 @@ namespace osu.Desktop }); } - private IBeatmapInfo getBeatmap(UserActivity activity) + private IBeatmapInfo? getBeatmap(UserActivity activity) { switch (activity) { @@ -183,10 +181,10 @@ namespace osu.Desktop switch (activity) { case UserActivity.InGame game: - return game.BeatmapInfo.ToString(); + return game.BeatmapInfo.ToString() ?? string.Empty; case UserActivity.Editing edit: - return edit.BeatmapInfo.ToString(); + return edit.BeatmapInfo.ToString() ?? string.Empty; case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs index 7b0bd69363..0ad68919a2 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationRequest.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Desktop.LegacyIpc { /// @@ -13,7 +11,7 @@ namespace osu.Desktop.LegacyIpc /// public class LegacyIpcDifficultyCalculationRequest { - public string BeatmapFile { get; set; } + public string BeatmapFile { get; set; } = string.Empty; public int RulesetId { get; set; } public int Mods { get; set; } } diff --git a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs index 6d36cbc4b6..7b9fae5797 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcDifficultyCalculationResponse.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - namespace osu.Desktop.LegacyIpc { /// diff --git a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs index 4df477191d..8d0add32d1 100644 --- a/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs +++ b/osu.Desktop/LegacyIpc/LegacyIpcMessage.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Platform; using Newtonsoft.Json.Linq; @@ -39,17 +37,20 @@ namespace osu.Desktop.LegacyIpc public new object Value { get => base.Value; - set => base.Value = new Data - { - MessageType = value.GetType().Name, - MessageData = value - }; + set => base.Value = new Data(value.GetType().Name, value); } public class Data { - public string MessageType { get; set; } - public object MessageData { get; set; } + public string MessageType { get; } + + public object MessageData { get; } + + public Data(string messageType, object messageData) + { + MessageType = messageType; + MessageData = messageData; + } } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index c7505e624c..5a1373e040 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.IO; using System.Runtime.Versioning; @@ -27,7 +25,7 @@ namespace osu.Desktop private const string base_game_name = @"osu"; #endif - private static LegacyTcpIpcProvider legacyIpc; + private static LegacyTcpIpcProvider? legacyIpc; [STAThread] public static void Main(string[] args) diff --git a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs index f0d95ba194..9959b24b35 100644 --- a/osu.Desktop/Security/ElevatedPrivilegesChecker.cs +++ b/osu.Desktop/Security/ElevatedPrivilegesChecker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Security.Principal; using osu.Framework; @@ -21,7 +19,7 @@ namespace osu.Desktop.Security public class ElevatedPrivilegesChecker : Component { [Resolved] - private INotificationOverlay notifications { get; set; } + private INotificationOverlay notifications { get; set; } = null!; private bool elevated; diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 4e5f8d37b1..d53db6c516 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.Versioning; using System.Threading.Tasks; @@ -26,8 +24,8 @@ namespace osu.Desktop.Updater [SupportedOSPlatform("windows")] public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager { - private UpdateManager updateManager; - private INotificationOverlay notificationOverlay; + private UpdateManager? updateManager; + private INotificationOverlay notificationOverlay = null!; public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); @@ -50,12 +48,12 @@ namespace osu.Desktop.Updater protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); - private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) + private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null) { // should we schedule a retry on completion of this check? bool scheduleRecheck = true; - const string github_token = null; // TODO: populate. + const string? github_token = null; // TODO: populate. try { @@ -145,7 +143,7 @@ namespace osu.Desktop.Updater private class UpdateCompleteNotification : ProgressCompletionNotification { [Resolved] - private OsuGame game { get; set; } + private OsuGame game { get; set; } = null!; public UpdateCompleteNotification(SquirrelUpdateManager updateManager) { @@ -154,7 +152,7 @@ namespace osu.Desktop.Updater Activated = () => { updateManager.PrepareUpdateAsync() - .ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit())); + .ContinueWith(_ => updateManager.Schedule(() => game.AttemptExit())); return true; }; } diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs index 0cb4ba9c04..284d25306d 100644 --- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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.Bindables; using osu.Framework.Graphics; @@ -14,12 +12,12 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyBlocker : Component { - private Bindable disableWinKey; - private IBindable localUserPlaying; - private IBindable isActive; + private Bindable disableWinKey = null!; + private IBindable localUserPlaying = null!; + private IBindable isActive = null!; [Resolved] - private GameHost host { get; set; } + private GameHost host { get; set; } = null!; [BackgroundDependencyLoader] private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config) diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index c69cce6200..1051e61f2f 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Runtime.InteropServices; @@ -21,7 +19,7 @@ namespace osu.Desktop.Windows private const int wm_syskeyup = 261; //Resharper disable once NotAccessedField.Local - private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC + private static LowLevelKeyboardProcDelegate? keyboardHookDelegate; // keeping a reference alive for the GC private static IntPtr keyHook; [StructLayout(LayoutKind.Explicit)] diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs new file mode 100644 index 0000000000..ff557638a9 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneBarLine : ManiaSkinnableTestScene + { + [Test] + public void TestMinor() + { + AddStep("Create barlines", () => recreate()); + } + + private void recreate(Func>? createBarLines = null) + { + var stageDefinitions = new List + { + new StageDefinition { Columns = 4 }, + }; + + SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s => + { + if (createBarLines != null) + { + var barLines = createBarLines(); + + foreach (var b in barLines) + s.Add(b); + + return; + } + + for (int i = 0; i < 64; i++) + { + s.Add(new BarLine + { + StartTime = Time.Current + i * 500, + Major = i % 4 == 0, + }); + } + })); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs index 614ef76a3b..73dfaaa878 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Acronym => "CS"; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.9; public override string Description => "No more tricky speed changes!"; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 0e2d6bce21..6020348938 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableBarLine : DrawableManiaHitObject { - /// - /// Height of major bar line triangles. - /// - private const float triangle_height = 12; - - /// - /// Offset of the major bar line triangles from the sides of the bar line. - /// - private const float triangle_offset = 9; - public DrawableBarLine(BarLine barLine) : base(barLine) { RelativeSizeAxes = Axes.X; - Height = 2f; + Height = barLine.Major ? 1.7f : 1.2f; AddInternal(new Box { @@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, - Colour = new Color4(255, 204, 33, 255), + Alpha = barLine.Major ? 0.5f : 0.2f }); if (barLine.Major) { - AddInternal(new EquilateralTriangle + Vector2 size = new Vector2(22, 6); + const float line_offset = 4; + + AddInternal(new Circle { - Name = "Left triangle", - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopCentre, - Size = new Vector2(triangle_height), - X = -triangle_offset, - Rotation = 90 + Name = "Left line", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + + Size = size, + X = -line_offset, }); - AddInternal(new EquilateralTriangle + AddInternal(new Circle { - Name = "Right triangle", - Anchor = Anchor.BottomRight, - Origin = Anchor.TopCentre, - Size = new Vector2(triangle_height), - X = triangle_offset, - Rotation = -90 + Name = "Right line", + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + Size = size, + X = line_offset, }); } - - if (!barLine.Major) - Alpha = 0.2f; } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 4e6493ba18..4b6f364831 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -9,7 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs index f185c8b99d..41e149ea2f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs index 7c4ab2f5f4..b6c8103e3c 100644 --- a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs @@ -6,7 +6,8 @@ using System.Diagnostics; using System.Linq; using Moq; using NUnit.Framework; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; @@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests [HeadlessTest] public class LegacyMainCirclePieceTest : OsuTestScene { + [Resolved] + private IRenderer renderer { get; set; } = null!; + private static readonly object?[][] texture_priority_cases = { // default priority lookup @@ -76,7 +80,12 @@ namespace osu.Game.Rulesets.Osu.Tests skin.Setup(s => s.GetTexture(It.IsAny())).CallBase(); skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny(), It.IsAny())) - .Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName }); + .Returns((string componentName, WrapMode _, WrapMode _) => + { + var tex = renderer.CreateTexture(1, 1); + tex.AssetName = componentName; + return tex; + }); Child = new DependencyProvidingContainer { @@ -84,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests Child = piece = new TestLegacyMainCirclePiece(priorityLookup), }; - var sprites = this.ChildrenOfType().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray(); + var sprites = this.ChildrenOfType().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray(); Debug.Assert(sprites.Length <= 2); }); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 46f7c461f8..7e995f2dde 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6369583000323935d, 206, "diffcalc-test")] - [TestCase(1.4476531024675374d, 45, "zero-length-sliders")] + [TestCase(6.7115569159190587d, 206, "diffcalc-test")] + [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.8816128335486386d, 206, "diffcalc-test")] - [TestCase(1.7540389962596916d, 45, "zero-length-sliders")] + [TestCase(8.9757300665532966d, 206, "diffcalc-test")] + [TestCase(1.7437232654020756d, 45, "zero-length-sliders")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.6369583000323935d, 239, "diffcalc-test")] - [TestCase(1.4476531024675374d, 54, "zero-length-sliders")] + [TestCase(6.7115569159190587d, 239, "diffcalc-test")] + [TestCase(1.4391311903612753d, 54, "zero-length-sliders")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index c8d2d07be5..d3e70a0a01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Testing.Input; using osu.Game.Audio; @@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneCursorTrail : OsuTestScene { + [Resolved] + private IRenderer renderer { get; set; } + [Test] public void TestSmoothCursorTrail() { @@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests { createTest(() => { - var skinContainer = new LegacySkinContainer(false); + var skinContainer = new LegacySkinContainer(renderer, false); var legacyCursorTrail = new LegacyCursorTrail(skinContainer); skinContainer.Child = legacyCursorTrail; @@ -58,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests { createTest(() => { - var skinContainer = new LegacySkinContainer(true); + var skinContainer = new LegacySkinContainer(renderer, true); var legacyCursorTrail = new LegacyCursorTrail(skinContainer); skinContainer.Child = legacyCursorTrail; @@ -82,10 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests [Cached(typeof(ISkinSource))] private class LegacySkinContainer : Container, ISkinSource { + private readonly IRenderer renderer; private readonly bool disjoint; - public LegacySkinContainer(bool disjoint) + public LegacySkinContainer(IRenderer renderer, bool disjoint) { + this.renderer = renderer; this.disjoint = disjoint; RelativeSizeAxes = Axes.Both; @@ -98,14 +103,14 @@ namespace osu.Game.Rulesets.Osu.Tests switch (componentName) { case "cursortrail": - var tex = new Texture(Texture.WhitePixel.TextureGL); + var tex = new Texture(renderer.WhitePixel); if (disjoint) tex.ScaleAdjust = 1 / 25f; return tex; case "cursormiddle": - return disjoint ? null : Texture.WhitePixel; + return disjoint ? null : renderer.WhitePixel; } return null; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 2edacee9ac..360815afbf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Input; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index dd110662b2..036f90b962 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Timing; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index 76d5ccf682..6d1b4d1a15 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators public static class AimEvaluator { private const double wide_angle_multiplier = 1.5; - private const double acute_angle_multiplier = 2.0; - private const double slider_multiplier = 1.5; + private const double acute_angle_multiplier = 1.95; + private const double slider_multiplier = 1.35; private const double velocity_change_multiplier = 0.75; /// @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); } - if (osuLastObj.TravelTime != 0) + if (osuLastObj.BaseObject is Slider) { // Reward sliders based on velocity. sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs index 3b0826394c..fcf4179a3b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs @@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators private const double max_opacity_bonus = 0.4; private const double hidden_bonus = 0.2; + private const double min_velocity = 0.5; + private const double slider_multiplier = 1.3; + /// /// Evaluates the difficulty of memorising and hitting an object, based on: /// - /// distance between the previous and current object, + /// distance between a number of previous objects and the current object, /// the visual opacity of the current object, + /// length and speed of the current object (for sliders), /// and whether the hidden mod is enabled. /// /// @@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators if (hidden) result *= 1.0 + hidden_bonus; + double sliderBonus = 0.0; + + if (osuCurrent.BaseObject is Slider osuSlider) + { + // Invert the scaling factor to determine the true travel distance independent of circle size. + double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor; + + // Reward sliders based on velocity. + sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5); + + // Longer sliders require more memorisation. + sliderBonus *= pixelTravelDistance; + + // Nerf sliders with repeats, as less memorisation is required. + if (osuSlider.RepeatCount > 0) + sliderBonus /= (osuSlider.RepeatCount + 1); + } + + result += sliderBonus * slider_multiplier; + return result; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 9f4a405113..0ebfb9a283 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -46,7 +46,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; if (mods.Any(h => h is OsuModRelax)) + { + aimRating *= 0.9; speedRating = 0.0; + flashlightRating *= 0.7; + } double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; @@ -62,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1 ); - double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; + double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index c3b7834009..3c82c2dc33 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); effectiveMissCount = calculateEffectiveMissCount(osuAttributes); - double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + double multiplier = PERFORMANCE_BASE_MULTIPLIER; if (score.Mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); @@ -51,10 +53,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) { - // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. - effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); + // https://www.desmos.com/calculator/bc9eybdthb + // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 + // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) + double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0); + double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0); - multiplier *= 0.6; + // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. + effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } double aimValue = computeAimValue(score, osuAttributes); @@ -103,7 +109,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); else if (attributes.ApproachRate < 8.0) - approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); + approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate); + + if (score.Mods.Any(h => h is OsuModRelax)) + approachRateFactor = 0.0; aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. @@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (score.Mods.Any(h => h is OsuModRelax)) + return 0.0; + double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + @@ -174,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); + speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); return speedValue; } @@ -266,6 +278,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; - private int totalSuccessfulHits => countGreat + countOk + countMeh; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index c8646ec456..c7c5650184 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { public class OsuDifficultyHitObject : DifficultyHitObject { - private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + /// + /// A distance by which all distances should be scaled in order to assume a uniform circle size. + /// + public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. + private const int min_delta_time = 25; - private const float maximum_slider_radius = normalised_radius * 2.4f; - private const float assumed_slider_radius = normalised_radius * 1.8f; + private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f; + private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; @@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double TravelDistance { get; private set; } /// - /// The time taken to travel through , with a minimum value of 25ms for a non-zero distance. + /// The time taken to travel through , with a minimum value of 25ms for objects. /// public double TravelTime { get; private set; } @@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (BaseObject is Slider currentSlider) { computeSliderCursorPosition(currentSlider); - TravelDistance = currentSlider.LazyTravelDistance; + // Bonus for repeat sliders until a better per nested object strain system can be achieved. + TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); } @@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing return; // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. - float scalingFactor = normalised_radius / (float)BaseObject.Radius; + float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius; if (BaseObject.Radius < 30) { @@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. var currCursorPosition = slider.StackedPosition; - double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. + double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. for (int i = 1; i < slider.NestedHitObjects.Count; i++) { @@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing else if (currMovementObj is SliderRepeat) { // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. - requiredMovement = normalised_radius; + requiredMovement = NORMALISED_RADIUS; } if (currMovementLength > requiredMovement) @@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (i == slider.NestedHitObjects.Count - 1) slider.LazyEndPosition = currCursorPosition; } - - slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved. } private Vector2 getEndCursorPosition(OsuHitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 9b1fbf9a2e..38e0e5b677 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.25; + private double skillMultiplier => 23.55; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 872fcf7e9b..9229c0393d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => OsuIcon.ModAutopilot; public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.1; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) }; public bool PerformFail() => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs index 9316f9ed74..7f7d6f70d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => FontAwesome.Solid.Magnet; public override ModType Type => ModType.Fun; public override string Description => "No need to chase the circles – your cursor is a magnet!"; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.5; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) }; private IFrameStableClock gameplayClock = null!; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 4949abccab..306b034645 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { InternalChildren = new Drawable[] { - connectionPool = new DrawablePool(1, 200), + connectionPool = new DrawablePool(10, 200), pointPool = new DrawablePool(50, 1000) }; } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index c1d518a843..1b2ef23674 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -9,9 +9,9 @@ using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Framework.Input; @@ -68,8 +68,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [BackgroundDependencyLoader] - private void load(ShaderManager shaders) + private void load(IRenderer renderer, ShaderManager shaders) { + texture ??= renderer.WhitePixel; shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); } @@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor resetTime(); } - private Texture texture = Texture.WhitePixel; + private Texture texture; public Texture Texture { @@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private Vector2 size; private Vector2 originPosition; - private readonly QuadBatch vertexBatch = new QuadBatch(max_sprites, 1); + private IVertexBatch vertexBatch; public TrailDrawNode(CursorTrail source) : base(source) @@ -254,15 +255,17 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Source.parts.CopyTo(parts, 0); } - public override void Draw(Action vertexAction) + public override void Draw(IRenderer renderer) { - base.Draw(vertexAction); + base.Draw(renderer); + + vertexBatch ??= renderer.CreateQuadBatch(max_sprites, 1); shader.Bind(); shader.GetUniform("g_FadeClock").UpdateValue(ref time); shader.GetUniform("g_FadeExponent").UpdateValue(ref fadeExponent); - texture.TextureGL.Bind(); + texture.Bind(); RectangleF textureRect = texture.GetTextureRect(); @@ -319,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { base.Dispose(isDisposing); - vertexBatch.Dispose(); + vertexBatch?.Dispose(); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3179b37d5a..fc2ba8ea2f 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -11,9 +11,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -112,21 +114,36 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader(true)] - private void load(OsuRulesetConfigManager config) + private void load(OsuRulesetConfigManager config, IBeatmap beatmap) { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); - RegisterPool(10, 100); + var osuBeatmap = (OsuBeatmap)beatmap; - RegisterPool(10, 100); - RegisterPool(10, 100); - RegisterPool(10, 100); - RegisterPool(10, 100); - RegisterPool(5, 50); + RegisterPool(20, 100); + + // handle edge cases where a beatmap has a slider with many repeats. + int maxRepeatsOnOneSlider = 0; + int maxTicksOnOneSlider = 0; + + if (osuBeatmap != null) + { + foreach (var slider in osuBeatmap.HitObjects.OfType()) + { + maxRepeatsOnOneSlider = Math.Max(maxRepeatsOnOneSlider, slider.RepeatCount); + maxTicksOnOneSlider = Math.Max(maxTicksOnOneSlider, slider.NestedHitObjects.OfType().Count()); + } + } + + RegisterPool(20, 100); + RegisterPool(20, 100); + RegisterPool(20, 100); + RegisterPool(Math.Max(maxTicksOnOneSlider, 20), Math.Max(maxTicksOnOneSlider, 200)); + RegisterPool(Math.Max(maxRepeatsOnOneSlider, 20), Math.Max(maxRepeatsOnOneSlider, 200)); RegisterPool(2, 20); - RegisterPool(10, 100); - RegisterPool(10, 100); + RegisterPool(10, 200); + RegisterPool(10, 200); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); @@ -173,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly Action onLoaded; public DrawableJudgementPool(HitResult result, Action onLoaded) - : base(10) + : base(20) { this.result = result; this.onLoaded = onLoaded; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs index 80f139a66b..d3bf70e603 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs @@ -6,8 +6,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Skinning; using osuTK.Graphics; diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs index af5588f509..f91e83a556 100644 --- a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs +++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs @@ -9,6 +9,7 @@ using System.Linq; using JetBrains.Annotations; using Moq; using NUnit.Framework; +using osu.Framework.Graphics.Rendering.Dummy; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; @@ -55,7 +56,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestAcceptable() { - var context = getContext(new Texture(1920, 1080)); + var context = getContext(new DummyRenderer().CreateTexture(1920, 1080)); Assert.That(check.Run(context), Is.Empty); } @@ -63,7 +64,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestTooHighResolution() { - var context = getContext(new Texture(3840, 2160)); + var context = getContext(new DummyRenderer().CreateTexture(3840, 2160)); var issues = check.Run(context).ToList(); @@ -74,7 +75,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestLowResolution() { - var context = getContext(new Texture(640, 480)); + var context = getContext(new DummyRenderer().CreateTexture(640, 480)); var issues = check.Run(context).ToList(); @@ -85,7 +86,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestTooLowResolution() { - var context = getContext(new Texture(100, 100)); + var context = getContext(new DummyRenderer().CreateTexture(100, 100)); var issues = check.Run(context).ToList(); @@ -96,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestTooUncompressed() { - var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3])); + var context = getContext(new DummyRenderer().CreateTexture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3])); var issues = check.Run(context).ToList(); @@ -107,7 +108,7 @@ namespace osu.Game.Tests.Editing.Checks [Test] public void TestStreamClosed() { - var background = new Texture(1920, 1080); + var background = new DummyRenderer().CreateTexture(1920, 1080); var stream = new Mock(new byte[1024 * 1024]); var context = getContext(background, stream.Object); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index c2df9e5e09..0e80f8f699 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -206,7 +206,7 @@ namespace osu.Game.Tests.Editing } private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) - => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); + => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance)); private void assertDurationToDistance(double duration, float expectedDistance) => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 1e10fbb2bc..9701a32951 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index a432cc9648..f05244ab88 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -11,8 +11,10 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Configuration; @@ -36,6 +38,9 @@ namespace osu.Game.Tests.Gameplay [Resolved] private OsuConfigManager config { get; set; } + [Resolved] + private GameHost host { get; set; } + [Test] public void TestRetrieveTopLevelSample() { @@ -202,6 +207,7 @@ namespace osu.Game.Tests.Gameplay #region IResourceStorageProvider + public IRenderer Renderer => host.Renderer; public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs index 461102124a..a229331ef0 100644 --- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs +++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs @@ -36,6 +36,23 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); } + [Test] + public void TestAudioEqualityCaseSensitivity() + { + var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1); + var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1); + + // empty by default so let's set it.. + beatmapSetA.Beatmaps.First().Metadata.AudioFile = "audio.mp3"; + beatmapSetB.Beatmaps.First().Metadata.AudioFile = "audio.mp3"; + + addAudioFile(beatmapSetA, "abc", "AuDiO.mP3"); + addAudioFile(beatmapSetB, "abc", "audio.mp3"); + + Assert.AreNotEqual(beatmapSetA, beatmapSetB); + Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); + } + [Test] public void TestAudioEqualitySameHash() { diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs index e21a3e636f..6297d9cdc7 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Framework.Timing; @@ -27,6 +27,9 @@ namespace osu.Game.Tests.NonVisual.Skinning private const string animation_name = "animation"; private const int frame_count = 6; + [Resolved] + private IRenderer renderer { get; set; } + [Cached(typeof(IAnimationTimeReference))] private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference(); @@ -35,9 +38,12 @@ namespace osu.Game.Tests.NonVisual.Skinning [Test] public void TestAnimationTimeReferenceChange() { - ISkin skin = new TestSkin(); + AddStep("get animation", () => + { + ISkin skin = new TestSkin(renderer); + Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false)); + }); - AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false))); AddAssert("frame count correct", () => animation.FrameCount == frame_count); assertPlaybackPosition(0); @@ -55,9 +61,16 @@ namespace osu.Game.Tests.NonVisual.Skinning { private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray(); + private readonly IRenderer renderer; + + public TestSkin(IRenderer renderer) + { + this.renderer = renderer; + } + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { - return lookup_names.Contains(componentName) ? Texture.WhitePixel : null; + return lookup_names.Contains(componentName) ? renderer.WhitePixel : null; } public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 22aa78838a..ca0d4d3cf3 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -11,6 +11,8 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Audio; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Dummy; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Database; @@ -141,6 +143,7 @@ namespace osu.Game.Tests.NonVisual.Skinning this.textureStore = textureStore; } + public IRenderer Renderer => new DummyRenderer(); public AudioManager AudioManager => null; public IResourceStore Files => null; public IResourceStore Resources => null; diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 67dbcf0ccf..7458508c7a 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -148,6 +149,16 @@ namespace osu.Game.Tests.Online Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d)); } + [Test] + public void TestSerialisedModSettingPresence() + { + var mod = new TestMod(); + + mod.TestSetting.Value = mod.TestSetting.Default; + JObject serialised = JObject.Parse(JsonConvert.SerializeObject(new APIMod(mod))); + Assert.False(serialised.ContainsKey("settings")); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index ee29cc8644..6bce03869d 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -128,6 +128,8 @@ namespace osu.Game.Tests.Resources var rulesetInfo = getRuleset(); + string hash = Guid.NewGuid().ToString().ComputeMD5Hash(); + yield return new BeatmapInfo { OnlineID = beatmapId, @@ -136,7 +138,8 @@ namespace osu.Game.Tests.Resources Length = length, BeatmapSet = beatmapSet, BPM = bpm, - Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Hash = hash, + MD5Hash = hash, Ruleset = rulesetInfo, Metadata = metadata.DeepClone(), Difficulty = new BeatmapDifficulty diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 9f732be9e3..29534348dc 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -15,11 +15,12 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; @@ -77,9 +78,9 @@ namespace osu.Game.Tests.Rulesets { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(ParentTextureStore = new TestTextureStore()); + dependencies.CacheAs(ParentTextureStore = new TestTextureStore(parent.Get().Renderer)); dependencies.CacheAs(ParentSampleStore = new TestSampleStore()); - dependencies.CacheAs(ParentShaderManager = new TestShaderManager()); + dependencies.CacheAs(ParentShaderManager = new TestShaderManager(parent.Get().Renderer)); return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); } @@ -95,6 +96,11 @@ namespace osu.Game.Tests.Rulesets private class TestTextureStore : TextureStore { + public TestTextureStore(IRenderer renderer) + : base(renderer) + { + } + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; public bool IsDisposed { get; private set; } @@ -148,8 +154,8 @@ namespace osu.Game.Tests.Rulesets private class TestShaderManager : ShaderManager { - public TestShaderManager() - : base(new ResourceStore()) + public TestShaderManager(IRenderer renderer) + : base(renderer, new ResourceStore()) { } diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs index f1ecd3b526..320373699d 100644 --- a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs +++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 53639deac3..c7eb334f25 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs index 1493c10969..004e253c1f 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs @@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 5452bfc939..b0c26f7280 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs index 1229b63a90..f7c88643fe 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs @@ -6,10 +6,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; @@ -21,6 +22,9 @@ namespace osu.Game.Tests.Skins [HeadlessTest] public class TestSceneSkinProvidingContainer : OsuTestScene { + [Resolved] + private IRenderer renderer { get; set; } + /// /// Ensures that the first inserted skin after resetting (via source change) /// is always prioritised over others when providing the same resource. @@ -35,7 +39,7 @@ namespace osu.Game.Tests.Skins { var sources = new List(); for (int i = 0; i < 10; i++) - sources.Add(new TestSkin()); + sources.Add(new TestSkin(renderer)); mostPrioritisedSource = sources.First(); @@ -76,12 +80,19 @@ namespace osu.Game.Tests.Skins { public const string TEXTURE_NAME = "virtual-texture"; + private readonly IRenderer renderer; + + public TestSkin(IRenderer renderer) + { + this.renderer = renderer; + } + public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException(); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { if (componentName == TEXTURE_NAME) - return Texture.WhitePixel; + return renderer.WhitePixel; return null; } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 42c1eeb6d1..22d6d08355 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -1,14 +1,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Rendering.Dummy; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Database; +using osu.Game.IO; using osu.Game.Skinning; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -19,9 +29,9 @@ namespace osu.Game.Tests.Skins public class TestSceneSkinResources : OsuTestScene { [Resolved] - private SkinManager skins { get; set; } + private SkinManager skins { get; set; } = null!; - private ISkin skin; + private ISkin skin = null!; [BackgroundDependencyLoader] private void load() @@ -32,5 +42,56 @@ namespace osu.Game.Tests.Skins [Test] public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null); + + [Test] + public void TestSampleRetrievalOrder() + { + Mock mockResourceProvider = null!; + Mock> mockResourceStore = null!; + List lookedUpFileNames = null!; + + AddStep("setup mock providers provider", () => + { + lookedUpFileNames = new List(); + mockResourceProvider = new Mock(); + mockResourceProvider.Setup(m => m.AudioManager).Returns(Audio); + mockResourceProvider.Setup(m => m.Renderer).Returns(new DummyRenderer()); + mockResourceStore = new Mock>(); + mockResourceStore.Setup(r => r.Get(It.IsAny())) + .Callback(n => lookedUpFileNames.Add(n)) + .Returns(null); + }); + + AddStep("query sample", () => + { + TestSkin testSkin = new TestSkin(new SkinInfo(), mockResourceProvider.Object, new ResourceStore(mockResourceStore.Object)); + testSkin.GetSample(new SampleInfo()); + }); + + AddAssert("sample lookups were in correct order", () => + { + string[] lookups = lookedUpFileNames.Where(f => f.StartsWith(TestSkin.SAMPLE_NAME, StringComparison.Ordinal)).ToArray(); + return Path.GetExtension(lookups[0]) == string.Empty + && Path.GetExtension(lookups[1]) == ".wav" + && Path.GetExtension(lookups[2]) == ".mp3" + && Path.GetExtension(lookups[3]) == ".ogg"; + }); + } + + private class TestSkin : Skin + { + public const string SAMPLE_NAME = "test-sample"; + + public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = "skin.ini") + : base(skin, resources, storage, configurationFilename) + { + } + + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); + + public override IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); + + public override ISample GetSample(ISampleInfo sampleInfo) => Samples.AsNonNull().Get(SAMPLE_NAME); + } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 5aaaca2b2c..d9dfa1d876 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Screens; using osu.Framework.Testing; @@ -43,6 +44,9 @@ namespace osu.Game.Tests.Visual.Background [Resolved] private OsuConfigManager config { get; set; } + [Resolved] + private IRenderer renderer { get; set; } + [SetUpSteps] public void SetUpSteps() { @@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.Background Id = API.LocalUser.Value.Id + 1, }); - private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio); + private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(renderer, Audio); private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio); private class TestBackgroundScreenDefault : BackgroundScreenDefault @@ -274,12 +278,15 @@ namespace osu.Game.Tests.Visual.Background private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap { - public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager) + private readonly IRenderer renderer; + + public UniqueBackgroundTestWorkingBeatmap(IRenderer renderer, AudioManager audioManager) : base(new Beatmap(), null, audioManager) { + this.renderer = renderer; } - protected override Texture GetBackground() => new Texture(1, 1); + protected override Texture GetBackground() => renderer.CreateTexture(1, 1); } private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index a44ab41d03..cce7ae1922 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -10,7 +10,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Game.Configuration; using osu.Game.Graphics.Backgrounds; @@ -28,11 +28,9 @@ namespace osu.Game.Tests.Visual.Background [Resolved] private SessionStatics statics { get; set; } - [Cached(typeof(LargeTextureStore))] - private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore(); - private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + private LookupLoggingTextureStore textureStore; private SeasonalBackgroundLoader backgroundLoader; private Container backgroundContainer; @@ -45,15 +43,32 @@ namespace osu.Game.Tests.Visual.Background "Backgrounds/bg3" }; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + textureStore = new LookupLoggingTextureStore(dependencies.Get()); + dependencies.CacheAs(typeof(LargeTextureStore), textureStore); + + return dependencies; + } + [BackgroundDependencyLoader] private void load(LargeTextureStore wrappedStore) { textureStore.AddStore(wrappedStore); - Add(backgroundContainer = new Container + Child = new DependencyProvidingContainer { - RelativeSizeAxes = Axes.Both - }); + CachedDependencies = new (Type, object)[] + { + (typeof(LargeTextureStore), textureStore) + }, + Child = backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both + } + }; } [SetUp] @@ -193,6 +208,11 @@ namespace osu.Game.Tests.Visual.Background { public List PerformedLookups { get; } = new List(); + public LookupLoggingTextureStore(IRenderer renderer) + : base(renderer) + { + } + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) { PerformedLookups.Add(name); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 4f7d3a4403..77e781bfe4 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -200,10 +200,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("click confirmation", () => { InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); + InputManager.PressButton(MouseButton.Left); }); assertCollectionCount(0); + + AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); } [Test] diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index cb29475ba5..94a71ed50c 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Components AddAssert("track stopped", () => !track.IsRunning); } - private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null); + private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(CreateAPIBeatmapSet()); private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 4baa4af8dd..80a5b4832b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -56,8 +56,10 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestCreateNewBeatmap() { + AddAssert("status is none", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false); + AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); } [Test] @@ -208,6 +210,8 @@ namespace osu.Game.Tests.Visual.Editing }); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); + AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); + AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => @@ -218,7 +222,7 @@ namespace osu.Game.Tests.Visual.Editing return beatmap != null && beatmap.DifficultyName == secondDifficultyName && set != null - && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); + && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); }); } @@ -294,7 +298,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); - AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); + AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddStep("save beatmap", () => Editor.Save()); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 3be6371f28..96ba802a5f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("reset clock", () => Clock.Seek(0)); - AddStep("start clock", Clock.Start); + AddStep("start clock", () => Clock.Start()); AddAssert("clock running", () => Clock.IsRunning); AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); - AddStep("start clock again", Clock.Start); + AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); } @@ -76,20 +76,20 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("reset clock", () => Clock.Seek(0)); - AddStep("stop clock", Clock.Stop); + AddStep("stop clock", () => Clock.Stop()); AddAssert("clock stopped", () => !Clock.IsRunning); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); - AddStep("start clock again", Clock.Start); + AddStep("start clock again", () => Clock.Start()); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); } [Test] public void TestClampWhenSeekOutsideBeatmapBounds() { - AddStep("stop clock", Clock.Stop); + AddStep("stop clock", () => Clock.Stop()); AddStep("seek before start time", () => Clock.Seek(-1000)); AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index c8ca273db5..d24baa6f63 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -60,17 +60,17 @@ namespace osu.Game.Tests.Visual.Editing // Forwards AddStep("Seek(0)", () => Clock.Seek(0)); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); AddStep("Seek(33)", () => Clock.Seek(33)); - AddAssert("Time = 33", () => Clock.CurrentTime == 33); + checkTime(33); AddStep("Seek(89)", () => Clock.Seek(89)); - AddAssert("Time = 89", () => Clock.CurrentTime == 89); + checkTime(89); // Backwards AddStep("Seek(25)", () => Clock.Seek(25)); - AddAssert("Time = 25", () => Clock.CurrentTime == 25); + checkTime(25); AddStep("Seek(0)", () => Clock.Seek(0)); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); } /// @@ -83,19 +83,19 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); } /// @@ -108,17 +108,17 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); } /// @@ -130,15 +130,15 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("SeekForward", () => Clock.SeekForward()); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("SeekForward", () => Clock.SeekForward()); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("SeekForward", () => Clock.SeekForward()); - AddAssert("Time = 200", () => Clock.CurrentTime == 200); + checkTime(200); AddStep("SeekForward", () => Clock.SeekForward()); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("SeekForward", () => Clock.SeekForward()); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); } /// @@ -150,17 +150,17 @@ namespace osu.Game.Tests.Visual.Editing reset(); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); } /// @@ -174,28 +174,28 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Seek(49)", () => Clock.Seek(49)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 150); + checkTime(150); AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("Seek(349)", () => Clock.Seek(349)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); AddStep("Seek(399)", () => Clock.Seek(399)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("Seek(449)", () => Clock.Seek(449)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); } /// @@ -208,15 +208,15 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 150", () => Clock.CurrentTime == 150); + checkTime(150); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("SeekBackward", () => Clock.SeekBackward()); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); } /// @@ -229,17 +229,17 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 350", () => Clock.CurrentTime == 350); + checkTime(350); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 175", () => Clock.CurrentTime == 175); + checkTime(175); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + checkTime(100); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + checkTime(50); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); } /// @@ -253,16 +253,16 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Seek(451)", () => Clock.Seek(451)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); AddStep("Seek(450.999)", () => Clock.Seek(450.999)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 450", () => Clock.CurrentTime == 450); + checkTime(450); AddStep("Seek(401)", () => Clock.Seek(401)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); AddStep("Seek(401.999)", () => Clock.Seek(401.999)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); - AddAssert("Time = 400", () => Clock.CurrentTime == 400); + checkTime(400); } /// @@ -297,9 +297,11 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); } - AddAssert("Time = 0", () => Clock.CurrentTime == 0); + checkTime(0); } + private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime)); + private void reset() { AddStep("Reset", () => Clock.Seek(0)); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs index 6f248f1247..924396ce03 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeeking.cs @@ -4,7 +4,6 @@ #nullable disable using NUnit.Framework; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; @@ -120,7 +119,7 @@ namespace osu.Game.Tests.Visual.Editing private void pressAndCheckTime(Key key, double expectedTime) { AddStep($"press {key}", () => InputManager.Key(key)); - AddUntilStep($"time is {expectedTime}", () => Precision.AlmostEquals(expectedTime, EditorClock.CurrentTime, 1)); + AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 16593effd6..d4fba76c37 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Game.Audio; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 36b07043dc..0ba112ec40 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -13,7 +13,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs new file mode 100644 index 0000000000..2901501b30 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API; +using osu.Game.Overlays.Toolbar; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneToolbarUserButton : OsuManualInputManagerTestScene + { + public TestSceneToolbarUserButton() + { + Container mainContainer; + + Children = new Drawable[] + { + mainContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = Toolbar.HEIGHT, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + new ToolbarUserButton(), + new Box + { + Colour = Color4.DarkRed, + RelativeSizeAxes = Axes.Y, + Width = 2, + }, + } + }, + } + }, + }; + + AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); + } + + [Test] + public void TestLoginLogout() + { + AddStep("Log out", () => ((DummyAPIAccess)API).Logout()); + AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang")); + } + + [Test] + public void TestStates() + { + AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang")); + + foreach (var state in Enum.GetValues()) + { + AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state)); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index d626426e6d..706d493fd6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -432,8 +432,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { var user = playingUsers.Single(u => u.UserID == userId); - OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull()); SpectatorClient.SendEndPlay(userId); + OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull()); playingUsers.Remove(user); }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs index 163f0e62df..7875a9dfbc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs @@ -83,6 +83,20 @@ namespace osu.Game.Tests.Visual.Online Beatmap = dummyBeatmap, }, new APIRecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Approved, + Beatmapset = dummyBeatmap, + }, + new APIRecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Loved, + Beatmapset = dummyBeatmap, + }, + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetApprove, @@ -90,6 +104,13 @@ namespace osu.Game.Tests.Visual.Online Beatmapset = dummyBeatmap, }, new APIRecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Ranked, + Beatmapset = dummyBeatmap, + }, + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetDelete, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index d0523b58fa..dadcd43db5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -150,13 +150,14 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); assertCollectionDropdownContains("1"); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); AddStep("add beatmap to collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Add(Beatmap.Value.BeatmapInfo.MD5Hash))); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); AddStep("remove beatmap from collection", () => writeAndRefresh(r => getFirstCollection().BeatmapMD5Hashes.Clear())); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } [Test] @@ -168,15 +169,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1")))); assertCollectionDropdownContains("1"); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); addClickAddOrRemoveButtonStep(1); AddAssert("collection contains beatmap", () => getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); + assertFirstButtonIs(FontAwesome.Solid.MinusSquare); addClickAddOrRemoveButtonStep(1); AddAssert("collection does not contain beatmap", () => !getFirstCollection().BeatmapMD5Hashes.Contains(Beatmap.Value.BeatmapInfo.MD5Hash)); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + assertFirstButtonIs(FontAwesome.Solid.PlusSquare); } [Test] @@ -226,6 +227,8 @@ namespace osu.Game.Tests.Visual.SongSelect => AddUntilStep($"collection dropdown header displays '{collectionName}'", () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); + private void assertFirstButtonIs(IconUsage icon) => AddUntilStep($"button is {icon.Icon.ToString()}", () => getAddOrRemoveButton(1).Icon.Equals(icon)); + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => AddUntilStep($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 593c8abac4..901f234db6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -9,29 +10,55 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneModPresetColumn : OsuTestScene + public class TestSceneModPresetColumn : OsuManualInputManagerTestScene { protected override bool UseFreshStoragePerRun => true; + private Container content = null!; + protected override Container Content => content; + private RulesetStore rulesets = null!; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Cached(typeof(IDialogOverlay))] + private readonly DialogOverlay dialogOverlay = new DialogOverlay(); + [BackgroundDependencyLoader] private void load() { Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(Realm); + + base.Content.AddRange(new Drawable[] + { + new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = content = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + } + }, + dialogOverlay + }); } [SetUpSteps] @@ -57,15 +84,10 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestBasicOperation() { AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); - AddStep("create content", () => Child = new Container + AddStep("create content", () => Child = new ModPresetColumn { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(30), - Child = new ModPresetColumn - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }); AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); @@ -112,15 +134,11 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestSoftDeleteSupport() { AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); - AddStep("create content", () => Child = new Container + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = new ModPresetColumn { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(30), - Child = new ModPresetColumn - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }); AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); @@ -136,9 +154,11 @@ namespace osu.Game.Tests.Visual.UserInterface foreach (var preset in r.All()) preset.DeletePending = true; })); - AddUntilStep("no panels visible", () => this.ChildrenOfType().Count() == 0); + AddUntilStep("no panels visible", () => !this.ChildrenOfType().Any()); - AddStep("undelete preset", () => Realm.Write(r => + AddStep("select mods from first preset", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }); + + AddStep("undelete presets", () => Realm.Write(r => { foreach (var preset in r.All()) preset.DeletePending = false; @@ -146,6 +166,101 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 panels visible", () => this.ChildrenOfType().Count() == 3); } + [Test] + public void TestAddingFlow() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddAssert("add preset button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() }); + AddAssert("add preset button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click add preset button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + OsuPopover? popover = null; + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddStep("attempt preset creation", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddWaitStep("wait some", 3); + AddAssert("preset creation did not occur", () => this.ChildrenOfType().Count() == 3); + AddUntilStep("popover is unchanged", () => this.ChildrenOfType().FirstOrDefault() == popover); + + AddStep("fill preset name", () => popover.ChildrenOfType().First().Current.Value = "new preset"); + AddStep("attempt preset creation", () => + { + InputManager.MoveMouseTo(popover.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + AddUntilStep("preset creation occurred", () => this.ChildrenOfType().Count() == 4); + + AddStep("click add preset button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().FirstOrDefault()) != null); + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); + } + + [Test] + public void TestDeleteFlow() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); + AddStep("right click first panel", () => + { + var panel = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Right); + }); + + AddUntilStep("wait for context menu", () => this.ChildrenOfType().Any()); + AddStep("click delete", () => + { + var deleteItem = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(deleteItem); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for dialog", () => dialogOverlay.CurrentDialog is DeleteModPresetDialog); + AddStep("hold confirm", () => + { + var confirmButton = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(confirmButton); + InputManager.PressButton(MouseButton.Left); + }); + AddUntilStep("wait for dialog to close", () => dialogOverlay.CurrentDialog == null); + AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddUntilStep("preset deletion occurred", () => this.ChildrenOfType().Count() == 2); + AddAssert("preset soft-deleted", () => Realm.Run(r => r.All().Count(preset => preset.DeletePending) == 1)); + } + private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 92d1cba2c2..bcd5579f03 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -23,6 +26,12 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset selected mods", () => SelectedMods.SetDefault()); + } + [Test] public void TestVariousModPresets() { @@ -37,6 +46,78 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestPresetSelectionStateAfterExternalModChanges() + { + ModPresetPanel? panel = null; + + AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f + }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR", () => SelectedMods.Value = new[] { new OsuModHardRock() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+customised DT", () => SelectedMods.Value = new Mod[] + { + new OsuModHardRock(), + new OsuModDoubleTime + { + SpeedChange = { Value = 1.25 } + } + }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); + + AddStep("customise mod in place", () => SelectedMods.Value.OfType().Single().SpeedChange.Value = 1.33); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + } + + [Test] + public void TestActivatingPresetTogglesIncludedMods() + { + ModPresetPanel? panel = null; + + AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f + }); + + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + + AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(Array.Empty()); + + AddStep("set different mod", () => SelectedMods.Value = new[] { new OsuModHidden() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + + AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); + } + + private void assertSelectedModsEquivalentTo(IEnumerable mods) + => AddAssert("selected mods changed correctly", () => new HashSet(SelectedMods.Value).SetEquals(mods)); + private static IEnumerable createTestPresets() => new[] { new ModPreset diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 4de70f6f9e..07473aa55b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value)); - ModColumn lastColumn = null; + ModSelectColumn lastColumn = null; AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value); AddStep("request scroll to last column", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index ee5ef2f364..e67eebf721 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -1,18 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; +using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics.Containers; using osu.Game.Overlays.Music; +using osu.Game.Rulesets; using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -21,13 +23,25 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene { - private readonly BindableList> beatmapSets = new BindableList>(); + protected override bool UseFreshStoragePerRun => true; - private PlaylistOverlay playlistOverlay; + private PlaylistOverlay playlistOverlay = null!; - private Live first; + private BeatmapManager beatmapManager = null!; - private const int item_count = 100; + private const int item_count = 20; + + private List beatmapSets => beatmapManager.GetAllUsableBeatmapSets(); + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default)); + Dependencies.Cache(Realm); + + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + } [SetUp] public void Setup() => Schedule(() => @@ -46,16 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface } }; - beatmapSets.Clear(); - for (int i = 0; i < item_count; i++) { - beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); + beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()); } - first = beatmapSets.First(); - - playlistOverlay.BeatmapSets.BindTo(beatmapSets); + beatmapSets.First().ToLive(Realm); }); [Test] @@ -70,9 +80,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); + PlaylistItem firstItem = null!; + AddStep("hold 1st item handle", () => { - var handle = this.ChildrenOfType>.PlaylistItemHandle>().First(); + firstItem = this.ChildrenOfType().First(); + var handle = firstItem.ChildrenOfType().First(); + InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); @@ -83,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft); }); - AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first)); + AddAssert("first is moved", () => playlistOverlay.ChildrenOfType().Single().Items.ElementAt(4).Value.Equals(firstItem.Model.Value)); AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); } @@ -101,6 +115,68 @@ namespace osu.Game.Tests.Visual.UserInterface () => playlistOverlay.ChildrenOfType() .Where(item => item.MatchingFilter) .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); + + AddStep("Import new non-matching beatmap", () => + { + var testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(1); + testBeatmapSetInfo.Beatmaps.Single().Metadata.Title = "no guid"; + beatmapManager.Import(testBeatmapSetInfo); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddAssert("results filtered correctly", + () => playlistOverlay.ChildrenOfType() + .Where(item => item.MatchingFilter) + .All(item => item.FilterTerms.Any(term => term.ToString().Contains("10")))); + } + + [Test] + public void TestCollectionFiltering() + { + NowPlayingCollectionDropdown collectionDropdown() => playlistOverlay.ChildrenOfType().Single(); + + AddStep("Add collection", () => + { + Dependencies.Get().Write(r => + { + r.RemoveAll(); + r.Add(new BeatmapCollection("wang")); + }); + }); + + AddUntilStep("wait for dropdown to have new collection", () => collectionDropdown().Items.Count() == 2); + + AddStep("Filter to collection", () => + { + collectionDropdown().Current.Value = collectionDropdown().Items.Last(); + }); + + AddUntilStep("No items present", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); + + AddStep("Import new non-matching beatmap", () => + { + beatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(1)); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddUntilStep("No items matching", () => !playlistOverlay.ChildrenOfType().Any(i => i.MatchingFilter)); + + BeatmapSetInfo collectionAddedBeatmapSet = null!; + + AddStep("Import new matching beatmap", () => + { + collectionAddedBeatmapSet = TestResources.CreateTestBeatmapSetInfo(1); + + beatmapManager.Import(collectionAddedBeatmapSet); + Realm.Write(r => r.All().First().BeatmapMD5Hashes.Add(collectionAddedBeatmapSet.Beatmaps.First().MD5Hash)); + }); + + AddStep("Force realm refresh", () => Realm.Run(r => r.Refresh())); + + AddUntilStep("Only matching item", + () => playlistOverlay.ChildrenOfType().Where(i => i.MatchingFilter).Select(i => i.Model.ID), () => Is.EquivalentTo(new[] { collectionAddedBeatmapSet.ID })); } } } diff --git a/osu.Game/Audio/PreviewTrack.cs b/osu.Game/Audio/PreviewTrack.cs index 7fb92f9f9d..2409ca6eb6 100644 --- a/osu.Game/Audio/PreviewTrack.cs +++ b/osu.Game/Audio/PreviewTrack.cs @@ -105,5 +105,11 @@ namespace osu.Game.Audio /// Retrieves the audio track. /// protected abstract Track? GetTrack(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + Track?.Dispose(); + } } } diff --git a/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs new file mode 100644 index 0000000000..767aa5df73 --- /dev/null +++ b/osu.Game/Beatmaps/BeatSyncProviderExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps +{ + public static class BeatSyncProviderExtensions + { + /// + /// Check whether beat sync is currently available. + /// + public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null; + + /// + /// Whether the beat sync provider is currently in a kiai section. Should make everything more epic. + /// + public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true; + } +} diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index f368f369ae..9196dde73c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -121,7 +121,8 @@ namespace osu.Game.Beatmaps OnlineID = -1; LastOnlineUpdate = null; OnlineMD5Hash = string.Empty; - Status = BeatmapOnlineStatus.None; + if (Status != BeatmapOnlineStatus.LocallyModified) + Status = BeatmapOnlineStatus.None; } #region Properties we may not want persisted (but also maybe no harm?) @@ -198,8 +199,8 @@ namespace osu.Game.Beatmaps Debug.Assert(x.BeatmapSet != null); Debug.Assert(y.BeatmapSet != null); - string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash; - string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash; + string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash; + string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash; return fileHashX == fileHashY; } diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index e7ff07fa5d..eab66b9857 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using osu.Framework.Localisation; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index cf763c53a7..e148016487 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps stream.Seek(0, SeekOrigin.Begin); // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. - var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); + var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null; string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); // ensure that two difficulties from the set don't point at the same beatmap file. @@ -313,9 +313,12 @@ namespace osu.Game.Beatmaps beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash(); + beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; + AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); setInfo.Hash = beatmapImporter.ComputeHash(setInfo); + setInfo.Status = BeatmapOnlineStatus.LocallyModified; Realm.Write(r => { diff --git a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs index ced85e0908..cdd99d4432 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineStatus.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineStatus.cs @@ -3,13 +3,23 @@ #nullable disable +using System.ComponentModel; using osu.Framework.Localisation; +using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Beatmaps { public enum BeatmapOnlineStatus { + /// + /// This is a special status given when local changes are made via the editor. + /// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted. + /// + [Description("Local")] + [LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))] + LocallyModified = -4, + None = -3, [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index d27d9d9192..b90dfdba05 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -84,13 +84,6 @@ namespace osu.Game.Beatmaps { } - /// - /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. - /// The path returned is relative to the user file storage. - /// - /// The name of the file to get the storage path of. - public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); - public bool Equals(BeatmapSetInfo? other) { if (ReferenceEquals(this, other)) return true; diff --git a/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs new file mode 100644 index 0000000000..965544da40 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Models; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapSetInfoExtensions + { + /// + /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// The lookup is case insensitive. + /// + /// The model to operate on. + /// The name of the file to get the storage path of. + public static string? GetPathForFile(this IHasRealmFiles model, string filename) => model.GetFile(filename)?.File.GetStoragePath(); + + /// + /// Returns the file usage for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// The lookup is case insensitive. + /// + /// The model to operate on. + /// The name of the file to get the storage path of. + public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) => + model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs index 02fb69b8f5..f96fcc2630 100644 --- a/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs +++ b/osu.Game/Beatmaps/BeatmapUpdaterMetadataLookup.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Data.Sqlite; using osu.Framework.Development; @@ -51,6 +52,9 @@ namespace osu.Game.Beatmaps /// /// Queue an update for a beatmap set. /// + /// + /// This may happen during initial import, or at a later stage in response to a user action or server event. + /// /// The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed). /// Whether metadata from an online source should be preferred. If true, the local cache will be skipped to ensure the freshest data state possible. public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch) @@ -89,21 +93,26 @@ namespace osu.Game.Beatmaps if (res != null) { - beatmapInfo.Status = res.Status; - - Debug.Assert(beatmapInfo.BeatmapSet != null); - - beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None; - beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID; - beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked; - beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted; - + beatmapInfo.OnlineID = res.OnlineID; beatmapInfo.OnlineMD5Hash = res.MD5Hash; beatmapInfo.LastOnlineUpdate = res.LastUpdated; - beatmapInfo.OnlineID = res.OnlineID; + Debug.Assert(beatmapInfo.BeatmapSet != null); + beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID; - beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; + // Some metadata should only be applied if there's no local changes. + if (shouldSaveOnlineMetadata(beatmapInfo)) + { + beatmapInfo.Status = res.Status; + beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; + } + + if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata)) + { + beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None; + beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked; + beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted; + } logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}."); } @@ -202,19 +211,26 @@ namespace osu.Game.Beatmaps { var status = (BeatmapOnlineStatus)reader.GetByte(2); - beatmapInfo.Status = status; + // Some metadata should only be applied if there's no local changes. + if (shouldSaveOnlineMetadata(beatmapInfo)) + { + beatmapInfo.Status = status; + beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3); + } - Debug.Assert(beatmapInfo.BeatmapSet != null); - - beatmapInfo.BeatmapSet.Status = status; - beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0); // TODO: DateSubmitted and DateRanked are not provided by local cache. beatmapInfo.OnlineID = reader.GetInt32(1); - beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3); - beatmapInfo.OnlineMD5Hash = reader.GetString(4); beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5); + Debug.Assert(beatmapInfo.BeatmapSet != null); + beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0); + + if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata)) + { + beatmapInfo.BeatmapSet.Status = status; + } + logForModel(set, $"Cached local retrieval for {beatmapInfo}."); return true; } @@ -233,6 +249,12 @@ namespace osu.Game.Beatmaps private void logForModel(BeatmapSetInfo set, string message) => RealmArchiveModelImporter.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}"); + /// + /// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it. + /// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick. + /// + private static bool shouldSaveOnlineMetadata(BeatmapInfo beatmapInfo) => beatmapInfo.MatchesOnlineVersion || beatmapInfo.Status != BeatmapOnlineStatus.LocallyModified; + public void Dispose() { cacheDownloadRequest?.Dispose(); diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index 88d4991c5d..23d90ab76e 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -6,15 +6,18 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Localisation; using osu.Game.Overlays; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class BeatmapSetOnlineStatusPill : CircularContainer + public class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip { private BeatmapOnlineStatus status; @@ -96,5 +99,19 @@ namespace osu.Game.Beatmaps.Drawables background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter; } + + public LocalisableString TooltipText + { + get + { + switch (Status) + { + case BeatmapOnlineStatus.LocallyModified: + return SongSelectStrings.LocallyModifiedTooltip; + } + + return string.Empty; + } + } } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs index 18eab09465..8ab632a757 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs @@ -118,7 +118,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons // another async load might have completed before this one. // if so, do not make any changes. if (loadedPreview != previewTrack) + { + loadedPreview.Dispose(); return; + } AddInternal(loadedPreview); toggleLoading(false); diff --git a/osu.Game/Beatmaps/IBeatSyncProvider.cs b/osu.Game/Beatmaps/IBeatSyncProvider.cs index 362f02f2dd..9ee19e720d 100644 --- a/osu.Game/Beatmaps/IBeatSyncProvider.cs +++ b/osu.Game/Beatmaps/IBeatSyncProvider.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio.Track; +using osu.Framework.Audio; using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -14,12 +14,16 @@ namespace osu.Game.Beatmaps /// Primarily intended for use with . /// [Cached] - public interface IBeatSyncProvider + public interface IBeatSyncProvider : IHasAmplitudes { + /// + /// Access any available control points from a beatmap providing beat sync. If null, no current provider is available. + /// ControlPointInfo? ControlPoints { get; } + /// + /// Access a clock currently responsible for providing beat sync. If null, no current provider is available. + /// IClock? Clock { get; } - - ChannelAmplitudes? Amplitudes { get; } } } diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index df44f01629..adb5f8c433 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -9,6 +9,8 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Dummy; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Lists; @@ -56,7 +58,7 @@ namespace osu.Game.Beatmaps this.resources = resources; this.host = host; this.files = files; - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); + largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files)); this.trackStore = trackStore; } @@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); AudioManager IStorageResourceProvider.AudioManager => audioManager; RealmAccess IStorageResourceProvider.RealmAccess => null; IResourceStore IStorageResourceProvider.Files => files; diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index f3f038a7f0..bf187265c1 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -3,33 +3,17 @@ using System; using Humanizer; -using osu.Framework.Graphics.Sprites; using osu.Game.Database; using osu.Game.Overlays.Dialog; namespace osu.Game.Collections { - public class DeleteCollectionDialog : PopupDialog + public class DeleteCollectionDialog : DeleteConfirmationDialog { public DeleteCollectionDialog(Live collection, Action deleteAction) { - HeaderText = "Confirm deletion of"; BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); - - Icon = FontAwesome.Regular.TrashAlt; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = @"Yes. Go for it.", - Action = deleteAction - }, - new PopupDialogCancelButton - { - Text = @"No! Abort mission!", - }, - }; + DeleteAction = deleteAction; } } } diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index b4a1fd78c3..f08b29cffe 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -3,16 +3,20 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum BackgroundSource { + [LocalisableDescription(typeof(SkinSettingsStrings), nameof(SkinSettingsStrings.SkinSectionHeader))] Skin, + + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.BeatmapHeader))] Beatmap, - [Description("Beatmap (with storyboard / video)")] + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))] BeatmapWithStoryboard, } } diff --git a/osu.Game/Configuration/DiscordRichPresenceMode.cs b/osu.Game/Configuration/DiscordRichPresenceMode.cs index 935619920a..150d23447e 100644 --- a/osu.Game/Configuration/DiscordRichPresenceMode.cs +++ b/osu.Game/Configuration/DiscordRichPresenceMode.cs @@ -3,17 +3,20 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum DiscordRichPresenceMode { + [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceOff))] Off, - [Description("Hide identifiable information")] + [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.HideIdentifiableInformation))] Limited, + [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceFull))] Full } } diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs index ec54bfcb71..9c69f33220 100644 --- a/osu.Game/Configuration/HUDVisibilityMode.cs +++ b/osu.Game/Configuration/HUDVisibilityMode.cs @@ -3,17 +3,20 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum HUDVisibilityMode { + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.NeverShowHUD))] Never, - [Description("Hide during gameplay")] + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.HideDuringGameplay))] HideDuringGameplay, + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.AlwaysShowHUD))] Always } } diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs index 9a02aaee60..052c6b4c55 100644 --- a/osu.Game/Configuration/RandomSelectAlgorithm.cs +++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs @@ -3,16 +3,17 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum RandomSelectAlgorithm { - [Description("Never repeat")] + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverRepeat))] RandomPermutation, - [Description("True Random")] + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.TrueRandom))] Random } } diff --git a/osu.Game/Configuration/ScalingMode.cs b/osu.Game/Configuration/ScalingMode.cs index 25c3692a1a..e0ad59746b 100644 --- a/osu.Game/Configuration/ScalingMode.cs +++ b/osu.Game/Configuration/ScalingMode.cs @@ -3,17 +3,23 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum ScalingMode { + [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScalingOff))] Off, + + [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverything))] Everything, - [Description("Excluding overlays")] + [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverythingExcludingOverlays))] ExcludeOverlays, + + [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleGameplay))] Gameplay, } } diff --git a/osu.Game/Configuration/ScreenshotFormat.cs b/osu.Game/Configuration/ScreenshotFormat.cs index 57c08538b8..13d0b64fd2 100644 --- a/osu.Game/Configuration/ScreenshotFormat.cs +++ b/osu.Game/Configuration/ScreenshotFormat.cs @@ -3,16 +3,17 @@ #nullable disable -using System.ComponentModel; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Configuration { public enum ScreenshotFormat { - [Description("JPG (web-friendly)")] + [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Jpg))] Jpg = 1, - [Description("PNG (lossless)")] + [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Png))] Png = 2 } } diff --git a/osu.Game/Configuration/SeasonalBackgroundMode.cs b/osu.Game/Configuration/SeasonalBackgroundMode.cs index 909798fe97..3e6d9e42aa 100644 --- a/osu.Game/Configuration/SeasonalBackgroundMode.cs +++ b/osu.Game/Configuration/SeasonalBackgroundMode.cs @@ -3,6 +3,9 @@ #nullable disable +using osu.Framework.Localisation; +using osu.Game.Localisation; + namespace osu.Game.Configuration { public enum SeasonalBackgroundMode @@ -10,16 +13,19 @@ namespace osu.Game.Configuration /// /// Seasonal backgrounds are shown regardless of season, if at all available. /// + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))] Always, /// /// Seasonal backgrounds are shown only during their corresponding season. /// + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))] Sometimes, /// /// Seasonal backgrounds are never shown. /// + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverSeasonalBackground))] Never } } diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 2adfe73d1e..79ea719583 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Models; namespace osu.Game.Database @@ -11,8 +12,16 @@ namespace osu.Game.Database /// public interface IHasRealmFiles { + /// + /// Available files in this model, with locally filenames. + /// When performing lookups, consider using or to do case-insensitive lookups. + /// IList Files { get; } + /// + /// A combined hash representing the model, based on the files it contains. + /// Implementation specific. + /// string Hash { get; set; } } } diff --git a/osu.Game/Database/LegacyImportManager.cs b/osu.Game/Database/LegacyImportManager.cs index 96f4aaf67c..78ebd8750e 100644 --- a/osu.Game/Database/LegacyImportManager.cs +++ b/osu.Game/Database/LegacyImportManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Threading; @@ -27,27 +25,30 @@ namespace osu.Game.Database public class LegacyImportManager : Component { [Resolved] - private SkinManager skins { get; set; } + private SkinManager skins { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private ScoreManager scores { get; set; } - - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + private ScoreManager scores { get; set; } = null!; [Resolved] - private IDialogOverlay dialogOverlay { get; set; } + private OsuGame? game { get; set; } [Resolved] - private RealmAccess realmAccess { get; set; } + private IDialogOverlay dialogOverlay { get; set; } = null!; - [Resolved(canBeNull: true)] - private DesktopGameHost desktopGameHost { get; set; } + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; - private StableStorage cachedStorage; + [Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms. + private DesktopGameHost? desktopGameHost { get; set; } + + [Resolved] + private INotificationOverlay? notifications { get; set; } + + private StableStorage? cachedStorage; public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; @@ -98,6 +99,9 @@ namespace osu.Game.Database stableStorage = GetCurrentStableStorage(); } + if (stableStorage == null) + return; + var importTasks = new List(); Task beatmapImportTask = Task.CompletedTask; @@ -108,7 +112,14 @@ namespace osu.Game.Database importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); if (content.HasFlagFast(StableContent.Collections)) - importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); + { + importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess) + { + // Other legacy importers import via model managers which handle the posting of notifications. + // Collections are an exception. + PostNotification = n => notifications?.Post(n) + }.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); + } if (content.HasFlagFast(StableContent.Scores)) importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); @@ -116,7 +127,7 @@ namespace osu.Game.Database await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); } - public StableStorage GetCurrentStableStorage() + public StableStorage? GetCurrentStableStorage() { if (cachedStorage != null) return cachedStorage; diff --git a/osu.Game/Database/ModelDownloader.cs b/osu.Game/Database/ModelDownloader.cs index edb8563c65..a3678602d1 100644 --- a/osu.Game/Database/ModelDownloader.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; using Humanizer; using osu.Framework.Logging; @@ -107,7 +108,15 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) - Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!"); + { + if (error is WebException webException && webException.Message == @"TooManyRequests") + { + notification.Close(); + PostNotification?.Invoke(new TooManyDownloadsNotification()); + } + else + Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!"); + } } } diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index 4224c92f2c..5c106aa493 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using osu.Framework.Platform; +using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Models; using osu.Game.Overlays.Notifications; @@ -79,7 +80,7 @@ namespace osu.Game.Database /// public void AddFile(TModel item, Stream contents, string filename, Realm realm) { - var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); + var existing = item.GetFile(filename); if (existing != null) { diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 5f0ec67c71..8099db44b1 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -25,6 +25,7 @@ using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using Realms; @@ -172,6 +173,11 @@ namespace osu.Game.Database if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) Filename += realm_extension; +#if DEBUG + if (!DebugUtils.IsNUnitRunning) + applyFilenameSchemaSuffix(ref Filename); +#endif + string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; // Attempt to recover a newer database version if available. @@ -211,6 +217,51 @@ namespace osu.Game.Database } } + /// + /// Some developers may be annoyed if a newer version migration (ie. caused by testing a pull request) + /// cause their test database to be unusable with previous versions. + /// To get around this, store development databases against their realm version. + /// Note that this means changes made on newer realm versions will disappear. + /// + private void applyFilenameSchemaSuffix(ref string filename) + { + string originalFilename = filename; + + filename = getVersionedFilename(schema_version); + + // First check if the current realm version already exists... + if (storage.Exists(filename)) + return; + + // Check for a previous version we can use as a base database to migrate from... + for (int i = schema_version - 1; i >= 0; i--) + { + string previousFilename = getVersionedFilename(i); + + if (storage.Exists(previousFilename)) + { + copyPreviousVersion(previousFilename, filename); + return; + } + } + + // Finally, check for a non-versioned file exists (aka before this method was added)... + if (storage.Exists(originalFilename)) + copyPreviousVersion(originalFilename, filename); + + void copyPreviousVersion(string previousFilename, string newFilename) + { + using (var previous = storage.GetStream(previousFilename)) + using (var current = storage.CreateFileSafely(newFilename)) + { + Logger.Log(@$"Copying previous realm database {previousFilename} to {newFilename} for migration to schema version {schema_version}"); + previous.CopyTo(current); + } + } + + string getVersionedFilename(int version) => originalFilename.Replace(realm_extension, $"_{version}{realm_extension}"); + } + private void attemptRecoverFromFile(string recoveryFilename) { Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database); @@ -292,6 +343,11 @@ namespace osu.Game.Database foreach (var s in pendingDeleteSkins) realm.Remove(s); + var pendingDeletePresets = realm.All().Where(s => s.DeletePending); + + foreach (var s in pendingDeletePresets) + realm.Remove(s); + transaction.Commit(); } diff --git a/osu.Game/Database/TooManyDownloadsNotification.cs b/osu.Game/Database/TooManyDownloadsNotification.cs new file mode 100644 index 0000000000..aa88fed43c --- /dev/null +++ b/osu.Game/Database/TooManyDownloadsNotification.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Overlays.Notifications; +using osu.Game.Resources.Localisation.Web; + +namespace osu.Game.Database +{ + public class TooManyDownloadsNotification : SimpleNotification + { + public TooManyDownloadsNotification() + { + Text = BeatmapsetsStrings.DownloadLimitExceeded; + Icon = FontAwesome.Solid.ExclamationCircle; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IconBackground.Colour = colours.RedDark; + } + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index b83df45bd7..1166a86814 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -14,9 +14,8 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL.Buffers; -using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Lists; namespace osu.Game.Graphics.Backgrounds @@ -88,7 +87,7 @@ namespace osu.Game.Graphics.Backgrounds private Random stableRandom; private IShader shader; - private readonly Texture texture; + private Texture texture; /// /// Construct a new triangle visualisation. @@ -98,13 +97,12 @@ namespace osu.Game.Graphics.Backgrounds { if (seed != null) stableRandom = new Random(seed.Value); - - texture = Texture.WhitePixel; } [BackgroundDependencyLoader] - private void load(ShaderManager shaders) + private void load(IRenderer renderer, ShaderManager shaders) { + texture = renderer.WhitePixel; shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); } @@ -184,8 +182,8 @@ namespace osu.Game.Graphics.Backgrounds private void addTriangles(bool randomY) { - // limited by the maximum size of QuadVertexBuffer for safety. - const int max_triangles = QuadVertexBuffer.MAX_QUADS; + // Limited by the maximum size of QuadVertexBuffer for safety. + const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2); AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio)); @@ -251,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds private readonly List parts = new List(); private Vector2 size; - private QuadBatch vertexBatch; + private IVertexBatch vertexBatch; public TrianglesDrawNode(Triangles source) : base(source) @@ -270,14 +268,14 @@ namespace osu.Game.Graphics.Backgrounds parts.AddRange(Source.parts); } - public override void Draw(Action vertexAction) + public override void Draw(IRenderer renderer) { - base.Draw(vertexAction); + base.Draw(renderer); if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) { vertexBatch?.Dispose(); - vertexBatch = new QuadBatch(Source.AimCount, 1); + vertexBatch = renderer.CreateQuadBatch(Source.AimCount, 1); } shader.Bind(); @@ -297,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(particle.Colour); - DrawTriangle( + renderer.DrawTriangle( texture, triangle, colourInfo, diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index e1998a1d7f..00fea601c6 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Diagnostics; using osu.Framework.Allocation; @@ -10,13 +8,12 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Screens.Play; namespace osu.Game.Graphics.Containers { /// /// A container which fires a callback when a new beat is reached. - /// Consumes a parent or (whichever is first available). + /// Consumes a parent . /// /// /// This container does not set its own clock to the source used for beat matching. @@ -28,8 +25,10 @@ namespace osu.Game.Graphics.Containers public class BeatSyncedContainer : Container { private int lastBeat; - protected TimingControlPoint LastTimingPoint { get; private set; } - protected EffectControlPoint LastEffectPoint { get; private set; } + + private TimingControlPoint? lastTimingPoint { get; set; } + + protected bool IsKiaiTime { get; private set; } /// /// The amount of time before a beat we should fire . @@ -71,12 +70,12 @@ namespace osu.Game.Graphics.Containers public double MinimumBeatLength { get; set; } /// - /// Whether this container is currently tracking a beatmap's timing data. + /// Whether this container is currently tracking a beat sync provider. /// protected bool IsBeatSyncedWithTrack { get; private set; } [Resolved] - protected IBeatSyncProvider BeatSyncSource { get; private set; } + protected IBeatSyncProvider BeatSyncSource { get; private set; } = null!; protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { @@ -87,19 +86,18 @@ namespace osu.Game.Graphics.Containers TimingControlPoint timingPoint; EffectControlPoint effectPoint; - IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null; + IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true; double currentTrackTime; if (IsBeatSyncedWithTrack) { - Debug.Assert(BeatSyncSource.ControlPoints != null); Debug.Assert(BeatSyncSource.Clock != null); currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds; - timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime); - effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime); + timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT; + effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT; } else { @@ -128,7 +126,7 @@ namespace osu.Game.Graphics.Containers TimeSinceLastBeat = beatLength - TimeUntilNextBeat; - if (ReferenceEquals(timingPoint, LastTimingPoint) && beatIndex == lastBeat) + if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat) return; // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. @@ -136,12 +134,13 @@ namespace osu.Game.Graphics.Containers if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE) { using (BeginDelayedSequence(-TimeSinceLastBeat)) - OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty); + OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes); } lastBeat = beatIndex; - LastTimingPoint = timingPoint; - LastEffectPoint = effectPoint; + lastTimingPoint = timingPoint; + + IsKiaiTime = effectPoint.KiaiMode; } } } diff --git a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs index a53b665857..78c8cbb79e 100644 --- a/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs +++ b/osu.Game/Graphics/OpenGL/Vertices/PositionAndColourVertex.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; -using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Rendering.Vertices; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 0e411876d3..6e2f460930 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -137,6 +137,9 @@ namespace osu.Game.Graphics { switch (status) { + case BeatmapOnlineStatus.LocallyModified: + return Color4.OrangeRed; + case BeatmapOnlineStatus.Ranked: case BeatmapOnlineStatus.Approved: return Color4Extensions.FromHex(@"b3ff66"); diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index c6f9c1343b..a902c8426f 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -6,8 +6,8 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -89,7 +89,7 @@ namespace osu.Game.Graphics currentTime = source.Time.Current; } - protected override void Blit(Action vertexAction) + protected override void Blit(IRenderer renderer) { double time = currentTime - startTime; @@ -112,9 +112,9 @@ namespace osu.Game.Graphics Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix) ); - DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, - new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), - null, TextureCoords); + renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), + inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), + textureCoords: TextureCoords); } } diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 06c9882d72..41a7d82e74 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -7,8 +7,8 @@ using System; using osu.Framework.Bindables; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -107,7 +107,7 @@ namespace osu.Game.Graphics sourceSize = Source.DrawSize; } - protected override void Blit(Action vertexAction) + protected override void Blit(IRenderer renderer) { foreach (var p in particles) { @@ -136,9 +136,9 @@ namespace osu.Game.Graphics transformPosition(rect.BottomRight, rect.Centre, angle) ); - DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, - new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), - null, TextureCoords); + renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), + inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), + textureCoords: TextureCoords); } } diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs index 13733bb3ad..7debdc7a37 100644 --- a/osu.Game/Graphics/Sprites/LogoAnimation.cs +++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs @@ -3,10 +3,9 @@ #nullable disable -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; @@ -57,11 +56,11 @@ namespace osu.Game.Graphics.Sprites progress = source.animationProgress; } - protected override void Blit(Action vertexAction) + protected override void Blit(IRenderer renderer) { - Shader.GetUniform("progress").UpdateValue(ref progress); + GetAppropriateShader(renderer).GetUniform("progress").UpdateValue(ref progress); - base.Blit(vertexAction); + base.Blit(renderer); } protected override bool CanDrawOpaqueInterior => false; diff --git a/osu.Game/Graphics/UserInterface/FPSCounter.cs b/osu.Game/Graphics/UserInterface/FPSCounter.cs index 539ac7ed1f..e9fb6a046a 100644 --- a/osu.Game/Graphics/UserInterface/FPSCounter.cs +++ b/osu.Game/Graphics/UserInterface/FPSCounter.cs @@ -167,6 +167,11 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); + // If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device) + // we want to ignore really long periods of no processing. + if (updateClock.ElapsedFrameTime > 10000) + return; + mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); // Handle the case where the window has become inactive or the user changed the @@ -177,15 +182,15 @@ namespace osu.Game.Graphics.UserInterface // use elapsed frame time rather then FramesPerSecond to better catch stutter frames. bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms; - // note that we use an elapsed time here of 1 intentionally. - // this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower. - displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1); + const float damp_time = 100; + + displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime); if (hasDrawSpike) // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. displayedFpsCount = 1000 / drawClock.ElapsedFrameTime; else - displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed); + displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed); if (Time.Current - lastUpdate > min_time_between_updates) { @@ -203,7 +208,7 @@ namespace osu.Game.Graphics.UserInterface if (hasSignificantChanges) requestDisplay(); - else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000) + else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered) { mainContent.FadeTo(0, 300, Easing.OutQuint); isDisplayed = false; diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index b3655eaab4..9f6177c226 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -20,6 +20,8 @@ namespace osu.Game.Graphics.UserInterface /// public class LoadingLayer : LoadingSpinner { + private readonly bool blockInput; + [CanBeNull] protected Box BackgroundDimLayer { get; } @@ -28,9 +30,11 @@ namespace osu.Game.Graphics.UserInterface /// /// Whether the full background area should be dimmed while loading. /// Whether the spinner should have a surrounding black box for visibility. - public LoadingLayer(bool dimBackground = false, bool withBox = true) + /// Whether to block input of components behind the loading layer. + public LoadingLayer(bool dimBackground = false, bool withBox = true, bool blockInput = true) : base(withBox) { + this.blockInput = blockInput; RelativeSizeAxes = Axes.Both; Size = new Vector2(1); @@ -52,6 +56,9 @@ namespace osu.Game.Graphics.UserInterface protected override bool Handle(UIEvent e) { + if (!blockInput) + return false; + switch (e) { // blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer. @@ -83,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface { base.Update(); - MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100)); + MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 100)); } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index 0bbcb2b976..9ef09d799e 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -49,15 +49,15 @@ namespace osu.Game.Graphics.UserInterface Active.BindDisabledChanged(disabled => Action = disabled ? null : Active.Toggle, true); Active.BindValueChanged(_ => { - updateActiveState(); + UpdateActiveState(); playSample(); }); - updateActiveState(); + UpdateActiveState(); base.LoadComplete(); } - private void updateActiveState() + protected virtual void UpdateActiveState() { DarkerColour = Active.Value ? ColourProvider.Highlight1 : ColourProvider.Background3; LighterColour = Active.Value ? ColourProvider.Colour0 : ColourProvider.Background1; diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs index 1cb6509cac..08982a8b5f 100644 --- a/osu.Game/IO/IStorageResourceProvider.cs +++ b/osu.Game/IO/IStorageResourceProvider.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Audio; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Database; @@ -12,10 +11,15 @@ namespace osu.Game.IO { public interface IStorageResourceProvider { + /// + /// The game renderer. + /// + IRenderer Renderer { get; } + /// /// Retrieve the game-wide audio manager. /// - AudioManager AudioManager { get; } + AudioManager? AudioManager { get; } /// /// Access game-wide user files. diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 7cd409c04c..14a3c5a43c 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -26,6 +26,11 @@ namespace osu.Game.IO /// public virtual string[] IgnoreFiles => Array.Empty(); + /// + /// A list of file/directory suffixes which should not be migrated. + /// + public virtual string[] IgnoreSuffixes => Array.Empty(); + protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) { @@ -73,6 +78,9 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; + if (IgnoreSuffixes.Any(suffix => fi.Name.EndsWith(suffix, StringComparison.Ordinal))) + continue; + allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false); } @@ -81,6 +89,9 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; + if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal))) + continue; + allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false); } @@ -101,6 +112,9 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreFiles.Contains(fileInfo.Name)) continue; + if (IgnoreSuffixes.Any(suffix => fileInfo.Name.EndsWith(suffix, StringComparison.Ordinal))) + continue; + AttemptOperation(() => { fileInfo.Refresh(); @@ -119,6 +133,9 @@ namespace osu.Game.IO if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; + if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal))) + continue; + CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); } } diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 368ac56850..f4c55e4b0e 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -38,15 +38,20 @@ namespace osu.Game.IO public override string[] IgnoreDirectories => new[] { "cache", - $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management", }; public override string[] IgnoreFiles => new[] { "framework.ini", "storage.ini", - $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note", - $"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock", + }; + + public override string[] IgnoreSuffixes => new[] + { + // Realm pipe files don't play well with copy operations + ".note", + ".lock", + ".management", }; public OsuStorage(GameHost host, Storage defaultStorage) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 1ee03a6964..ad53f6d90f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; @@ -15,10 +13,11 @@ namespace osu.Game.Input.Bindings { public class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput { - private readonly Drawable handler; - private InputManager parentInputManager; + private readonly Drawable? handler; - public GlobalActionContainer(OsuGameBase game) + private InputManager? parentInputManager; + + public GlobalActionContainer(OsuGameBase? game) : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { if (game is IKeyBindingHandler) @@ -32,7 +31,10 @@ namespace osu.Game.Input.Bindings parentInputManager = GetContainingInputManager(); } + // IMPORTANT: Do not change the order of key bindings in this list. + // It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer). public override IEnumerable DefaultKeyBindings => GlobalKeyBindings + .Concat(OverlayKeyBindings) .Concat(EditorKeyBindings) .Concat(InGameKeyBindings) .Concat(SongSelectKeyBindings) @@ -40,25 +42,6 @@ namespace osu.Game.Input.Bindings public IEnumerable GlobalKeyBindings => new[] { - new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying), - new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), - new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial), - new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons), - new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), - - new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), - new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), - new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), - new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), - new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), - new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), - - new KeyBinding(InputKey.Escape, GlobalAction.Back), - new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), - - new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home), - new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), @@ -69,7 +52,32 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), + new KeyBinding(InputKey.Escape, GlobalAction.Back), + new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), + + new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home), + + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay), + new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor), + new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile), + + new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin), + + new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons), + new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot), + }; + + public IEnumerable OverlayKeyBindings => new[] + { + new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), + new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying), + new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial), + new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), + new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), + new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), }; public IEnumerable EditorKeyBindings => new[] @@ -332,5 +340,8 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))] ToggleFPSDisplay, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))] + ToggleProfile, } } diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 2a93ac1cd6..2d914ac6e0 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -3,8 +3,9 @@ #nullable disable -using System.ComponentModel; using osu.Framework.Input; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Input { @@ -17,18 +18,20 @@ namespace osu.Game.Input /// /// The mouse cursor will be free to move outside the game window. /// + [LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.NeverConfine))] Never, /// /// The mouse cursor will be locked to the window bounds during gameplay, /// but may otherwise move freely. /// - [Description("During Gameplay")] + [LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.ConfineDuringGameplay))] DuringGameplay, /// /// The mouse cursor will always be locked to the window bounds while the game has focus. /// + [LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.AlwaysConfine))] Always } } diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 1ee562e122..93e3276f59 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; @@ -89,6 +89,16 @@ namespace osu.Game.Localisation /// public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections"); + /// + /// "Name" + /// + public static LocalisableString Name => new TranslatableString(getKey(@"name"), @"Name"); + + /// + /// "Description" + /// + public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"Description"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs b/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs new file mode 100644 index 0000000000..33738fe95e --- /dev/null +++ b/osu.Game/Localisation/DeleteConfirmationDialogStrings.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . 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 DeleteConfirmationDialogStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog"; + + /// + /// "Confirm deletion of" + /// + public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Confirm deletion of"); + + /// + /// "Yes. Go for it." + /// + public static LocalisableString Confirm => new TranslatableString(getKey(@"confirm"), @"Yes. Go for it."); + + /// + /// "No! Abort mission" + /// + public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"No! Abort mission"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 8a0f773551..13cfcc3a19 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -104,6 +104,31 @@ namespace osu.Game.Localisation /// public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled"); + /// + /// "Hide during gameplay" + /// + public static LocalisableString HideDuringGameplay => new TranslatableString(getKey(@"hide_during_gameplay"), @"Hide during gameplay"); + + /// + /// "Always" + /// + public static LocalisableString AlwaysShowHUD => new TranslatableString(getKey(@"always_show_hud"), @"Always"); + + /// + /// "Never" + /// + public static LocalisableString NeverShowHUD => new TranslatableString(getKey(@"never_show_hud"), @"Never"); + + /// + /// "Standardised" + /// + public static LocalisableString StandardisedScoreDisplay => new TranslatableString(getKey(@"standardised_score_display"), @"Standardised"); + + /// + /// "Classic" + /// + public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index de1a5b189c..172818c1c0 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -149,6 +149,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications"); + /// + /// "Toggle profile" + /// + public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile"); + /// /// "Pause gameplay" /// diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 38355d9041..2ab9d9de87 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -129,6 +129,16 @@ namespace osu.Game.Localisation /// public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration"); + /// + /// "JPG (web-friendly)" + /// + public static LocalisableString Jpg => new TranslatableString(getKey(@"jpg_web_friendly"), @"JPG (web-friendly)"); + + /// + /// "PNG (lossless)" + /// + public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index e46b4cecf3..a0da6cc2f7 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global"); + /// + /// "Overlays" + /// + public static LocalisableString OverlaysSection => new TranslatableString(getKey(@"overlays_section"), @"Overlays"); + /// /// "Song Select" /// diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs index b4326b8e39..9b8b207c47 100644 --- a/osu.Game/Localisation/LayoutSettingsStrings.cs +++ b/osu.Game/Localisation/LayoutSettingsStrings.cs @@ -29,6 +29,26 @@ namespace osu.Game.Localisation /// public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."); + /// + /// "Excluding overlays" + /// + public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays"); + + /// + /// "Everything" + /// + public static LocalisableString ScaleEverything => new TranslatableString(getKey(@"scale_everything"), @"Everything"); + + /// + /// "Gameplay" + /// + public static LocalisableString ScaleGameplay => new TranslatableString(getKey(@"scale_gameplay"), @"Gameplay"); + + /// + /// "Off" + /// + public static LocalisableString ScalingOff => new TranslatableString(getKey(@"scaling_off"), @"Off"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index 7a04bcd1ca..a398eced07 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs @@ -74,6 +74,16 @@ namespace osu.Game.Localisation /// public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps"); + /// + /// "Delete ALL mod presets" + /// + public static LocalisableString DeleteAllModPresets => new TranslatableString(getKey(@"delete_all_mod_presets"), @"Delete ALL mod presets"); + + /// + /// "Restore all recently deleted mod presets" + /// + public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index 3696b1f2cd..d6a01c4794 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -29,6 +29,11 @@ namespace osu.Game.Localisation /// public static LocalisableString PersonalPresets => new TranslatableString(getKey(@"personal_presets"), @"Personal Presets"); + /// + /// "Add preset" + /// + public static LocalisableString AddPreset => new TranslatableString(getKey(@"add_preset"), @"Add preset"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index fd7225ad2e..1772f03b29 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -64,6 +64,21 @@ namespace osu.Game.Localisation /// public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now."); + /// + /// "Always" + /// + public static LocalisableString AlwaysConfine => new TranslatableString(getKey(@"always_confine"), @"Always"); + + /// + /// "During Gameplay" + /// + public static LocalisableString ConfineDuringGameplay => new TranslatableString(getKey(@"confine_during_gameplay"), @"During Gameplay"); + + /// + /// "Never" + /// + public static LocalisableString NeverConfine => new TranslatableString(getKey(@"never_confine"), @"Never"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index 6862f4ac2c..3200b1c75c 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.cs @@ -64,6 +64,21 @@ namespace osu.Game.Localisation /// public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results"); + /// + /// "Hide identifiable information" + /// + public static LocalisableString HideIdentifiableInformation => new TranslatableString(getKey(@"hide_identifiable_information"), @"Hide identifiable information"); + + /// + /// "Full" + /// + public static LocalisableString DiscordPresenceFull => new TranslatableString(getKey(@"discord_presence_full"), @"Full"); + + /// + /// "Off" + /// + public static LocalisableString DiscordPresenceOff => new TranslatableString(getKey(@"discord_presence_off"), @"Off"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index a356c9e20b..bc4be56c80 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -14,6 +14,21 @@ namespace osu.Game.Localisation /// public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); + /// + /// "None" + /// + public static LocalisableString BorderNone => new TranslatableString(getKey(@"no_borders"), @"None"); + + /// + /// "Corners" + /// + public static LocalisableString BorderCorners => new TranslatableString(getKey(@"corner_borders"), @"Corners"); + + /// + /// "Full" + /// + public static LocalisableString BorderFull => new TranslatableString(getKey(@"full_borders"), @"Full"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs new file mode 100644 index 0000000000..12f70cd967 --- /dev/null +++ b/osu.Game/Localisation/SongSelectStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . 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 SongSelectStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.SongSelect"; + + /// + /// "Local" + /// + public static LocalisableString LocallyModified => new TranslatableString(getKey(@"locally_modified"), @"Local"); + + /// + /// "Has been locally modified" + /// + public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/ToolbarStrings.cs b/osu.Game/Localisation/ToolbarStrings.cs new file mode 100644 index 0000000000..6dc8a1e50c --- /dev/null +++ b/osu.Game/Localisation/ToolbarStrings.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . 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 ToolbarStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.Toolbar"; + + /// + /// "Connection interrupted, will try to reconnect..." + /// + public static LocalisableString AttemptingToReconnect => new TranslatableString(getKey(@"attempting_to_reconnect"), @"Connection interrupted, will try to reconnect..."); + + /// + /// "Connecting..." + /// + public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting..."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index a090b8c14c..ea664d7b50 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -114,6 +114,46 @@ namespace osu.Game.Localisation /// public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit"); + /// + /// "Beatmap (with storyboard / video)" + /// + public static LocalisableString BeatmapWithStoryboard => new TranslatableString(getKey(@"beatmap_with_storyboard"), @"Beatmap (with storyboard / video)"); + + /// + /// "Always" + /// + public static LocalisableString AlwaysSeasonalBackground => new TranslatableString(getKey(@"always_seasonal_backgrounds"), @"Always"); + + /// + /// "Never" + /// + public static LocalisableString NeverSeasonalBackground => new TranslatableString(getKey(@"never_seasonal_backgrounds"), @"Never"); + + /// + /// "Sometimes" + /// + public static LocalisableString SometimesSeasonalBackground => new TranslatableString(getKey(@"sometimes_seasonal_backgrounds"), @"Sometimes"); + + /// + /// "Sequential" + /// + public static LocalisableString SequentialHotkeyStyle => new TranslatableString(getKey(@"mods_sequential_hotkeys"), @"Sequential"); + + /// + /// "Classic" + /// + public static LocalisableString ClassicHotkeyStyle => new TranslatableString(getKey(@"mods_classic_hotkeys"), @"Classic"); + + /// + /// "Never repeat" + /// + public static LocalisableString NeverRepeat => new TranslatableString(getKey(@"never_repeat_random"), @"Never repeat"); + + /// + /// "True Random" + /// + public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random"); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 7af19f6dd1..a0c8e0d555 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -104,120 +104,39 @@ namespace osu.Game.Online.API /// private int failureCount; + /// + /// The main API thread loop, which will continue to run until the game is shut down. + /// private void run() { while (!cancellationToken.IsCancellationRequested) { - switch (State.Value) + if (state.Value == APIState.Failing) { - case APIState.Failing: - //todo: replace this with a ping request. - log.Add(@"In a failing state, waiting a bit before we try again..."); - Thread.Sleep(5000); + // To recover from a failing state, falling through and running the full reconnection process seems safest for now. + // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads. + log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again..."); + Thread.Sleep(5000); + } - if (!IsLoggedIn) goto case APIState.Connecting; + // Ensure that we have valid credentials. + // If not, setting the offline state will allow the game to prompt the user to provide new credentials. + if (!HasLogin) + { + state.Value = APIState.Offline; + Thread.Sleep(50); + continue; + } - if (queue.Count == 0) - { - log.Add(@"Queueing a ping request"); - Queue(new GetUserRequest()); - } + Debug.Assert(HasLogin); - break; + // Ensure that we are in an online state. If not, attempt a connect. + if (state.Value != APIState.Online) + { + attemptConnect(); - case APIState.Offline: - case APIState.Connecting: - // work to restore a connection... - if (!HasLogin) - { - state.Value = APIState.Offline; - Thread.Sleep(50); - continue; - } - - state.Value = APIState.Connecting; - - // save the username at this point, if the user requested for it to be. - config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - - if (!authentication.HasValidAccessToken) - { - LastLoginError = null; - - try - { - authentication.AuthenticateWithLogin(ProvidedUsername, password); - } - catch (Exception e) - { - //todo: this fails even on network-related issues. we should probably handle those differently. - LastLoginError = e; - log.Add(@"Login failed!"); - password = null; - authentication.Clear(); - continue; - } - } - - var userReq = new GetUserRequest(); - - userReq.Failure += ex => - { - if (ex is APIException) - { - LastLoginError = ex; - log.Add("Login failed on local user retrieval!"); - Logout(); - } - else if (ex is WebException webException && webException.Message == @"Unauthorized") - { - log.Add(@"Login no longer valid"); - Logout(); - } - else - failConnectionProcess(); - }; - userReq.Success += u => - { - localUser.Value = u; - - // todo: save/pull from settings - localUser.Value.Status.Value = new UserStatusOnline(); - - failureCount = 0; - }; - - if (!handleRequest(userReq)) - { - failConnectionProcess(); - continue; - } - - // getting user's friends is considered part of the connection process. - var friendsReq = new GetFriendsRequest(); - - friendsReq.Failure += _ => failConnectionProcess(); - friendsReq.Success += res => - { - friends.AddRange(res); - - //we're connected! - state.Value = APIState.Online; - }; - - if (!handleRequest(friendsReq)) - { - failConnectionProcess(); - continue; - } - - // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. - // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests - // before actually going online. - while (State.Value > APIState.Offline && State.Value < APIState.Online) - Thread.Sleep(500); - - break; + if (state.Value != APIState.Online) + continue; } // hard bail if we can't get a valid access token. @@ -227,31 +146,132 @@ namespace osu.Game.Online.API continue; } - while (true) - { - APIRequest req; - - lock (queue) - { - if (queue.Count == 0) break; - - req = queue.Dequeue(); - } - - handleRequest(req); - } - + processQueuedRequests(); Thread.Sleep(50); } + } - void failConnectionProcess() + /// + /// Dequeue from the queue and run each request synchronously until the queue is empty. + /// + private void processQueuedRequests() + { + while (true) { - // if something went wrong during the connection process, we want to reset the state (but only if still connecting). - if (State.Value == APIState.Connecting) - state.Value = APIState.Failing; + APIRequest req; + + lock (queue) + { + if (queue.Count == 0) return; + + req = queue.Dequeue(); + } + + handleRequest(req); } } + /// + /// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends. + /// + /// + /// This method takes control of and transitions from to either + /// - (successful connection) + /// - (failed connection but retrying) + /// - (failed and can't retry, clear credentials and require user interaction) + /// + /// Whether the connection attempt was successful. + private void attemptConnect() + { + state.Value = APIState.Connecting; + + if (localUser.IsDefault) + { + // Show a placeholder user if saved credentials are available. + // This is useful for storing local scores and showing a placeholder username after starting the game, + // until a valid connection has been established. + setLocalUser(new APIUser + { + Username = ProvidedUsername, + }); + } + + // save the username at this point, if the user requested for it to be. + config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); + + if (!authentication.HasValidAccessToken) + { + LastLoginError = null; + + try + { + authentication.AuthenticateWithLogin(ProvidedUsername, password); + } + catch (Exception e) + { + //todo: this fails even on network-related issues. we should probably handle those differently. + LastLoginError = e; + log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!"); + + Logout(); + return; + } + } + + var userReq = new GetUserRequest(); + userReq.Failure += ex => + { + if (ex is APIException) + { + LastLoginError = ex; + log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!"); + Logout(); + } + else if (ex is WebException webException && webException.Message == @"Unauthorized") + { + log.Add(@"Login no longer valid"); + Logout(); + } + else + { + state.Value = APIState.Failing; + } + }; + userReq.Success += user => + { + // todo: save/pull from settings + user.Status.Value = new UserStatusOnline(); + + setLocalUser(user); + + // we're connected! + state.Value = APIState.Online; + failureCount = 0; + }; + + if (!handleRequest(userReq)) + { + state.Value = APIState.Failing; + return; + } + + var friendsReq = new GetFriendsRequest(); + friendsReq.Failure += _ => state.Value = APIState.Failing; + friendsReq.Success += res => friends.AddRange(res); + + if (!handleRequest(friendsReq)) + { + state.Value = APIState.Failing; + return; + } + + // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. + // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests + // before actually going online. + while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested) + Thread.Sleep(500); + } + public void Perform(APIRequest request) { try @@ -327,8 +347,7 @@ namespace osu.Game.Online.API if (req.CompletionState != APIRequestCompletionState.Completed) return false; - // we could still be in initialisation, at which point we don't want to say we're Online yet. - if (IsLoggedIn) state.Value = APIState.Online; + // Reset failure count if this request succeeded. failureCount = 0; return true; } @@ -402,7 +421,7 @@ namespace osu.Game.Online.API } } - public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect? + public bool IsLoggedIn => State.Value > APIState.Offline; public void Queue(APIRequest request) { @@ -442,7 +461,7 @@ namespace osu.Game.Online.API // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => { - localUser.Value = createGuestUser(); + setLocalUser(createGuestUser()); friends.Clear(); }); @@ -452,6 +471,8 @@ namespace osu.Game.Online.API private static APIUser createGuestUser() => new GuestUser(); + private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false); + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 900f59290c..dcbaaea012 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using MessagePack; using Newtonsoft.Json; @@ -23,7 +20,7 @@ namespace osu.Game.Online.API { [JsonProperty("acronym")] [Key(0)] - public string Acronym { get; set; } + public string Acronym { get; set; } = string.Empty; [JsonProperty("settings")] [Key(1)] @@ -49,7 +46,7 @@ namespace osu.Game.Online.API } } - public Mod ToMod([NotNull] Ruleset ruleset) + public Mod ToMod(Ruleset ruleset) { Mod resultMod = ruleset.CreateModFromAcronym(Acronym); @@ -80,6 +77,8 @@ namespace osu.Game.Online.API return resultMod; } + public bool ShouldSerializeSettings() => Settings.Count > 0; + public bool Equals(APIMod other) { if (ReferenceEquals(null, other)) return false; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 812aa7f09f..a90b11e354 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -13,19 +13,16 @@ namespace osu.Game.Online.API { /// /// The local user. - /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindable LocalUser { get; } /// /// The user's friends. - /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindableList Friends { get; } /// /// The current user's activity. - /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindable Activity { get; } diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 6558578023..e2e5ea4239 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -102,6 +102,14 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("pp")] public double? PP { get; set; } + public bool ShouldSerializeID() => false; + public bool ShouldSerializeUser() => false; + public bool ShouldSerializeBeatmap() => false; + public bool ShouldSerializeBeatmapSet() => false; + public bool ShouldSerializePP() => false; + public bool ShouldSerializeOnlineID() => false; + public bool ShouldSerializeHasReplay() => false; + #endregion public override string ToString() => $"score_id: {ID} user_id: {UserID}"; @@ -165,7 +173,7 @@ namespace osu.Game.Online.API.Requests.Responses RulesetID = score.RulesetID, Passed = score.Passed, Mods = score.APIMods, - Statistics = score.Statistics, + Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; public long OnlineID => ID ?? -1; diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 603bd10c38..04b87c19da 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -196,6 +196,9 @@ namespace osu.Game.Online.Multiplayer APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem)); APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId); + // The server will null out the end date upon the host joining the room, but the null value is never communicated to the client. + APIRoom.EndDate.Value = null; + Debug.Assert(LocalUser != null); addUserToAPIRoom(LocalUser); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 78cc4d7f70..f7747c5d64 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1138,6 +1138,13 @@ namespace osu.Game mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; + case GlobalAction.ToggleProfile: + if (userProfile.State.Value == Visibility.Visible) + userProfile.Hide(); + else + ShowUser(API.LocalUser.Value); + return true; + case GlobalAction.RandomSkin: // Don't allow random skin selection while in the skin editor. // This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 97d15ae6ad..8d5c58d5f0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -248,7 +248,7 @@ namespace osu.Game dependencies.CacheAs(Storage); - var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); + var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore())); dependencies.Cache(largeStore); @@ -588,6 +588,6 @@ namespace osu.Game ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null; IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index cfa53009ef..3136492af0 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -104,11 +104,11 @@ namespace osu.Game.Overlays filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged()); apiUser = api.LocalUser.GetBoundCopy(); - apiUser.BindValueChanged(_ => + apiUser.BindValueChanged(_ => Schedule(() => { if (api.IsLoggedIn) addContentToResultsArea(Drawable.Empty()); - }); + })); } public void ShowWithSearch(string query) diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs index e840d0e82c..0ce55ce549 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PlayButton.cs @@ -147,7 +147,10 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { // beatmapset may have changed. if (Preview != preview) + { + preview?.Dispose(); return; + } AddInternal(preview); loading = false; diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index e94a1b0147..8fc011b2bf 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -4,7 +4,6 @@ #nullable disable using Markdig.Syntax; -using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; @@ -12,16 +11,8 @@ namespace osu.Game.Overlays.Comments { public class CommentMarkdownContainer : OsuMarkdownContainer { - public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); - protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); - private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer - { - // Don't render image in comment for now - protected override void AddImage(LinkInline linkInline) { } - } - private class CommentMarkdownHeading : OsuMarkdownHeading { public CommentMarkdownHeading(HeadingBlock headingBlock) diff --git a/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs b/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs new file mode 100644 index 0000000000..fd26dd7e8e --- /dev/null +++ b/osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Dialog +{ + /// + /// Base class for various confirmation dialogs that concern deletion actions. + /// Differs from in that the confirmation button is a "dangerous" one + /// (requires the confirm button to be held). + /// + public abstract class DeleteConfirmationDialog : PopupDialog + { + /// + /// The action which performs the deletion. + /// + protected Action? DeleteAction { get; set; } + + protected DeleteConfirmationDialog() + { + HeaderText = DeleteConfirmationDialogStrings.HeaderText; + + Icon = FontAwesome.Regular.TrashAlt; + + Buttons = new PopupDialogButton[] + { + new PopupDialogDangerousButton + { + Text = DeleteConfirmationDialogStrings.Confirm, + Action = () => DeleteAction?.Invoke() + }, + new PopupDialogCancelButton + { + Text = DeleteConfirmationDialogStrings.Cancel + } + }; + } + } +} diff --git a/osu.Game/Overlays/Login/LoginPanel.cs b/osu.Game/Overlays/Login/LoginPanel.cs index 1d8926b991..32a7fca1a6 100644 --- a/osu.Game/Overlays/Login/LoginPanel.cs +++ b/osu.Game/Overlays/Login/LoginPanel.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Users; using osuTK; @@ -109,7 +110,7 @@ namespace osu.Game.Overlays.Login Origin = Anchor.TopCentre, TextAnchor = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - Text = state.NewValue == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ", + Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting, Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, }; diff --git a/osu.Game/Overlays/Mods/AddPresetButton.cs b/osu.Game/Overlays/Mods/AddPresetButton.cs new file mode 100644 index 0000000000..1242088cf5 --- /dev/null +++ b/osu.Game/Overlays/Mods/AddPresetButton.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public class AddPresetButton : ShearedToggleButton, IHasPopover + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + public AddPresetButton() + : base(1) + { + RelativeSizeAxes = Axes.X; + Height = ModSelectPanel.HEIGHT; + + // shear will be applied at a higher level in `ModPresetColumn`. + Content.Shear = Vector2.Zero; + Padding = new MarginPadding(); + + Text = "+"; + TextSize = 30; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedMods.BindValueChanged(mods => Enabled.Value = mods.NewValue.Any(), true); + Enabled.BindValueChanged(enabled => + { + if (!enabled.NewValue) + Active.Value = false; + }); + } + + protected override void UpdateActiveState() + { + DarkerColour = Active.Value ? colours.Orange1 : ColourProvider.Background3; + LighterColour = Active.Value ? colours.Orange0 : ColourProvider.Background1; + TextColour = Active.Value ? ColourProvider.Background6 : ColourProvider.Content1; + + if (Active.Value) + this.ShowPopover(); + else + this.HidePopover(); + } + + public Popover GetPopover() => new AddPresetPopover(this); + } +} diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs new file mode 100644 index 0000000000..8188c98e46 --- /dev/null +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + internal class AddPresetPopover : OsuPopover + { + private readonly AddPresetButton button; + + private readonly LabelledTextBox nameTextBox; + private readonly LabelledTextBox descriptionTextBox; + private readonly ShearedButton createButton; + + [Resolved] + private Bindable ruleset { get; set; } = null!; + + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + public AddPresetPopover(AddPresetButton addPresetButton) + { + button = addPresetButton; + + Child = new FillFlowContainer + { + Width = 300, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(7), + Children = new Drawable[] + { + nameTextBox = new LabelledTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Label = CommonStrings.Name, + TabbableContentContainer = this + }, + descriptionTextBox = new LabelledTextBox + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Label = CommonStrings.Description, + TabbableContentContainer = this + }, + createButton = new ShearedButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = ModSelectOverlayStrings.AddPreset, + Action = tryCreatePreset + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + Body.BorderThickness = 3; + Body.BorderColour = colours.Orange1; + + createButton.DarkerColour = colours.Orange1; + createButton.LighterColour = colours.Orange0; + createButton.TextColour = colourProvider.Background6; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); + } + + private void tryCreatePreset() + { + if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value)) + { + Body.Shake(); + return; + } + + realm.Write(r => r.Add(new ModPreset + { + Name = nameTextBox.Current.Value, + Description = descriptionTextBox.Current.Value, + Mods = selectedMods.Value.ToArray(), + Ruleset = r.Find(ruleset.Value.ShortName) + })); + + this.HidePopover(); + } + + protected override void UpdateState(ValueChangedEvent state) + { + base.UpdateState(state); + if (state.NewValue == Visibility.Hidden) + button.Active.Value = false; + } + } +} diff --git a/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs new file mode 100644 index 0000000000..b3a19e35ce --- /dev/null +++ b/osu.Game/Overlays/Mods/DeleteModPresetDialog.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Database; +using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Overlays.Mods +{ + public class DeleteModPresetDialog : DeleteConfirmationDialog + { + public DeleteModPresetDialog(Live modPreset) + { + BodyText = modPreset.PerformRead(preset => preset.Name); + DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true); + } + } +} diff --git a/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs b/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs index 6375b37f8b..abb7a804da 100644 --- a/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs +++ b/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Localisation; namespace osu.Game.Overlays.Mods.Input { @@ -15,6 +17,7 @@ namespace osu.Game.Overlays.Mods.Input /// Individual letters in a row trigger the mods in a sequential fashion. /// Uses . /// + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SequentialHotkeyStyle))] Sequential, /// @@ -22,6 +25,7 @@ namespace osu.Game.Overlays.Mods.Input /// One keybinding can toggle between what used to be s on stable, /// and some mods in a column may not have any hotkeys at all. /// + [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.ClassicHotkeyStyle))] Classic } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 1c40c8c6e5..7a2c727a00 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Mods private IModHotkeyHandler hotkeyHandler = null!; private Task? latestLoadTask; - internal bool ItemsLoaded => latestLoadTask == null; + internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; public ModColumn(ModType modType, bool allowIncompatibleSelection) { @@ -132,18 +132,11 @@ namespace osu.Game.Overlays.Mods var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)); - Task? loadTask; - - latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => + latestLoadTask = LoadComponentsAsync(panels, loaded => { ItemsFlow.ChildrenEnumerable = loaded; updateState(); }, (cancellationTokenSource = new CancellationTokenSource()).Token); - loadTask.ContinueWith(_ => - { - if (loadTask == latestLoadTask) - latestLoadTask = null; - }); } private void updateState() diff --git a/osu.Game/Overlays/Mods/ModPresetColumn.cs b/osu.Game/Overlays/Mods/ModPresetColumn.cs index bed4cff0ea..176c527a10 100644 --- a/osu.Game/Overlays/Mods/ModPresetColumn.cs +++ b/osu.Game/Overlays/Mods/ModPresetColumn.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Localisation; @@ -31,6 +31,10 @@ namespace osu.Game.Overlays.Mods { AccentColour = colours.Orange1; HeaderText = ModSelectOverlayStrings.PersonalPresets; + + AddPresetButton addPresetButton; + ItemsFlow.Add(addPresetButton = new AddPresetButton()); + ItemsFlow.SetLayoutPosition(addPresetButton, float.PositiveInfinity); } protected override void LoadComplete() @@ -46,44 +50,41 @@ namespace osu.Game.Overlays.Mods { presetSubscription?.Dispose(); presetSubscription = realm.RegisterForNotifications(r => - r.All() - .Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0" - + $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName) - .OrderBy(preset => preset.Name), - (presets, _, _) => asyncLoadPanels(presets)); + r.All() + .Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0" + + $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName) + .OrderBy(preset => preset.Name), asyncLoadPanels); } private CancellationTokenSource? cancellationTokenSource; private Task? latestLoadTask; - internal bool ItemsLoaded => latestLoadTask == null; + internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true; - private void asyncLoadPanels(IReadOnlyList presets) + private void asyncLoadPanels(IRealmCollection presets, ChangeSet changes, Exception error) { cancellationTokenSource?.Cancel(); if (!presets.Any()) { - ItemsFlow.Clear(); + removeAndDisposePresetPanels(); return; } - var panels = presets.Select(preset => new ModPresetPanel(preset.ToLive(realm)) + latestLoadTask = LoadComponentsAsync(presets.Select(p => new ModPresetPanel(p.ToLive(realm)) { Shear = Vector2.Zero - }); - - Task? loadTask; - - latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => + }), loaded => { - ItemsFlow.ChildrenEnumerable = loaded; + removeAndDisposePresetPanels(); + ItemsFlow.AddRange(loaded); }, (cancellationTokenSource = new CancellationTokenSource()).Token); - loadTask.ContinueWith(_ => + + void removeAndDisposePresetPanels() { - if (loadTask == latestLoadTask) - latestLoadTask = null; - }); + foreach (var panel in ItemsFlow.OfType().ToArray()) + panel.RemoveAndDisposeImmediately(); + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index a00729d9fd..a259645479 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -1,27 +1,44 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip + public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip, IHasContextMenu { public readonly Live Preset; public override BindableBool Active { get; } = new BindableBool(); + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + public ModPresetPanel(Live preset) { Preset = preset; Title = preset.Value.Name; Description = preset.Value.Description; + + Action = toggleRequestedByUser; } [BackgroundDependencyLoader] @@ -30,7 +47,58 @@ namespace osu.Game.Overlays.Mods AccentColour = colours.Orange1; } + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedMods.BindValueChanged(_ => selectedModsChanged(), true); + } + + private void toggleRequestedByUser() + { + // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections. + // if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset + // (there are no other active mods than what the preset specifies, and the mod settings match exactly). + // therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off. + selectedMods.Value = !Active.Value + ? Preset.Value.Mods.ToArray() + : Array.Empty(); + } + + private void selectedModsChanged() + { + settingChangeTracker?.Dispose(); + settingChangeTracker = new ModSettingChangeTracker(selectedMods.Value); + settingChangeTracker.SettingChanged = _ => updateActiveState(); + updateActiveState(); + } + + private void updateActiveState() + { + Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); + } + + #region IHasCustomTooltip + public ModPreset TooltipContent => Preset.Value; public ITooltip GetCustomTooltip() => new ModPresetTooltip(ColourProvider); + + #endregion + + #region IHasContextMenu + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))) + }; + + #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + settingChangeTracker?.Dispose(); + } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index afc6775e83..adc008e1f7 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -11,12 +11,14 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -72,6 +74,11 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool AllowCustomisation => true; + /// + /// Whether the column with available mod presets should be shown. + /// + protected virtual bool ShowPresets => false; + protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; @@ -139,40 +146,37 @@ namespace osu.Game.Overlays.Mods MainAreaContent.AddRange(new Drawable[] { - new Container + new OsuContextMenuContainer { - Padding = new MarginPadding - { - Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING, - Bottom = PADDING - }, RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Children = new Drawable[] + Child = new PopoverContainer { - columnScroll = new ColumnScrollContainer + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - Masking = false, - ClampExtension = 100, - ScrollbarOverlapsContent = false, - Child = columnFlow = new ColumnFlowContainer + Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING, + Bottom = PADDING + }, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Children = new Drawable[] + { + columnScroll = new ColumnScrollContainer { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Shear = new Vector2(SHEAR, 0), - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Margin = new MarginPadding { Horizontal = 70 }, - Padding = new MarginPadding { Bottom = 10 }, - Children = new[] + RelativeSizeAxes = Axes.Both, + Masking = false, + ClampExtension = 100, + ScrollbarOverlapsContent = false, + Child = columnFlow = new ColumnFlowContainer { - createModColumnContent(ModType.DifficultyReduction), - createModColumnContent(ModType.DifficultyIncrease), - createModColumnContent(ModType.Automation), - createModColumnContent(ModType.Conversion), - createModColumnContent(ModType.Fun) + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Shear = new Vector2(SHEAR, 0), + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Margin = new MarginPadding { Horizontal = 70 }, + Padding = new MarginPadding { Bottom = 10 }, + ChildrenEnumerable = createColumns() } } } @@ -269,7 +273,7 @@ namespace osu.Game.Overlays.Mods /// public void SelectAll() { - foreach (var column in columnFlow.Columns) + foreach (var column in columnFlow.Columns.OfType()) column.SelectAll(); } @@ -278,10 +282,27 @@ namespace osu.Game.Overlays.Mods /// public void DeselectAll() { - foreach (var column in columnFlow.Columns) + foreach (var column in columnFlow.Columns.OfType()) column.DeselectAll(); } + private IEnumerable createColumns() + { + if (ShowPresets) + { + yield return new ColumnDimContainer(new ModPresetColumn + { + Margin = new MarginPadding { Right = 10 } + }); + } + + yield return createModColumnContent(ModType.DifficultyReduction); + yield return createModColumnContent(ModType.DifficultyIncrease); + yield return createModColumnContent(ModType.Automation); + yield return createModColumnContent(ModType.Conversion); + yield return createModColumnContent(ModType.Fun); + } + private ColumnDimContainer createModColumnContent(ModType modType) { var column = CreateModColumn(modType).With(column => @@ -290,12 +311,7 @@ namespace osu.Game.Overlays.Mods column.Margin = new MarginPadding { Right = 10 }; }); - return new ColumnDimContainer(column) - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140), - }; + return new ColumnDimContainer(column); } private void createLocalMods() @@ -317,7 +333,7 @@ namespace osu.Game.Overlays.Mods AvailableMods.Value = newLocalAvailableMods; filterMods(); - foreach (var column in columnFlow.Columns) + foreach (var column in columnFlow.Columns.OfType()) column.AvailableMods = AvailableMods.Value.GetValueOrDefault(column.ModType, Array.Empty()); } @@ -458,7 +474,7 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value); + bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value); double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double duration = allFiltered ? 0 : fade_in_duration; @@ -516,14 +532,19 @@ namespace osu.Game.Overlays.Mods { var column = columnFlow[i].Column; - bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value); + bool allFiltered = false; + + if (column is ModColumn modColumn) + { + allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); + modColumn.FlushPendingSelections(); + } double duration = allFiltered ? 0 : fade_out_duration; float newYPosition = 0; if (!allFiltered) newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; - column.FlushPendingSelections(); column.TopLevelContent .MoveToY(newYPosition, duration, Easing.OutQuint) .FadeOut(duration, Easing.OutQuint); @@ -589,6 +610,7 @@ namespace osu.Game.Overlays.Mods /// /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility. /// + [Cached] internal class ColumnScrollContainer : OsuScrollContainer { public ColumnScrollContainer() @@ -632,7 +654,7 @@ namespace osu.Game.Overlays.Mods /// internal class ColumnFlowContainer : FillFlowContainer { - public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); + public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); public override void Add(ColumnDimContainer dimContainer) { @@ -648,7 +670,7 @@ namespace osu.Game.Overlays.Mods /// internal class ColumnDimContainer : Container { - public ModColumn Column { get; } + public ModSelectColumn Column { get; } /// /// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen. @@ -663,12 +685,21 @@ namespace osu.Game.Overlays.Mods [Resolved] private OsuColour colours { get; set; } = null!; - public ColumnDimContainer(ModColumn column) + public ColumnDimContainer(ModSelectColumn column) { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + Child = Column = column; column.Active.BindTo(Active); } + [BackgroundDependencyLoader] + private void load(ColumnScrollContainer columnScroll) + { + RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -677,7 +708,7 @@ namespace osu.Game.Overlays.Mods FinishTransforms(); } - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || (Column as ModColumn)?.SelectionAnimationRunning == true; private void updateState() { diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index abf327a388..b3df00f8f9 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -43,8 +43,7 @@ namespace osu.Game.Overlays.Mods } public const float CORNER_RADIUS = 7; - - protected const float HEIGHT = 42; + public const float HEIGHT = 42; protected virtual float IdleSwitchWidth => 14; protected virtual float ExpandedSwitchWidth => 30; diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 2bb0ff1085..15fc54a337 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using osu.Framework.Bindables; @@ -17,10 +15,12 @@ namespace osu.Game.Overlays.Music { public class Playlist : OsuRearrangeableListContainer> { - public Action> RequestSelection; + public Action>? RequestSelection; public readonly Bindable> SelectedSet = new Bindable>(); + private FilterCriteria currentCriteria = new FilterCriteria(); + public new MarginPadding Padding { get => base.Padding; @@ -31,26 +31,22 @@ namespace osu.Game.Overlays.Music { var items = (SearchContainer>>)ListContainer; - string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); + string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray()); foreach (var item in items.OfType()) { - if (currentCollectionHashes == null) - item.InSelectedCollection = true; - else - { - item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash) - .Any(currentCollectionHashes.Contains); - } + item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains); } items.SearchTerm = criteria.SearchText; + currentCriteria = criteria; } - public Live FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); + public Live? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); protected override OsuRearrangeableListItem> CreateOsuDrawable(Live item) => new PlaylistItem(item) { + InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false, SelectedSet = { BindTarget = SelectedSet }, RequestSelection = set => RequestSelection?.Invoke(set) }; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index e33fc8064f..9fe2fd5279 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -26,8 +26,6 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; - public IBindableList> BeatmapSets => beatmapSets; - private readonly BindableList> beatmapSets = new BindableList>(); private readonly Bindable beatmap = new Bindable(); @@ -104,9 +102,7 @@ namespace osu.Game.Overlays.Music { base.LoadComplete(); - // tests might bind externally, in which case we don't want to involve realm. - if (beatmapSets.Count == 0) - beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); + beatmapSubscription = realm.RegisterForNotifications(r => r.All().Where(s => !s.DeletePending), beatmapsChanged); list.Items.BindTo(beatmapSets); beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 943c105008..6b71ae73e3 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -120,7 +120,13 @@ namespace osu.Game.Overlays.Profile.Sections.Recent }; default: - return Empty(); + return new RecentActivityIcon(activity) + { + RelativeSizeAxes = Axes.X, + Height = 11, + FillMode = FillMode.Fit, + Margin = new MarginPadding { Top = 2, Vertical = 2 } + }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/RecentActivityIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/RecentActivityIcon.cs new file mode 100644 index 0000000000..c55eedd1ed --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/RecentActivityIcon.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class RecentActivityIcon : Container + { + private readonly SpriteIcon icon; + private readonly APIRecentActivity activity; + + public RecentActivityIcon(APIRecentActivity activity) + { + this.activity = activity; + Child = icon = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + // references: + // https://github.com/ppy/osu-web/blob/659b371dcadf25b4f601a4c9895a813078301084/resources/assets/lib/profile-page/parse-event.tsx + // https://github.com/ppy/osu-web/blob/master/resources/assets/less/bem/profile-extra-entries.less#L98-L128 + switch (activity.Type) + { + case RecentActivityType.BeatmapPlaycount: + icon.Icon = FontAwesome.Solid.Play; + icon.Colour = Color4.White; + break; + + case RecentActivityType.BeatmapsetApprove: + icon.Icon = FontAwesome.Solid.Check; + icon.Colour = getColorForApprovalType(activity.Approval); + break; + + case RecentActivityType.BeatmapsetDelete: + icon.Icon = FontAwesome.Solid.TrashAlt; + icon.Colour = colours.Red1; + break; + + case RecentActivityType.BeatmapsetRevive: + icon.Icon = FontAwesome.Solid.TrashRestore; + icon.Colour = Color4.White; + break; + + case RecentActivityType.BeatmapsetUpdate: + icon.Icon = FontAwesome.Solid.SyncAlt; + icon.Colour = colours.Green1; + break; + + case RecentActivityType.BeatmapsetUpload: + icon.Icon = FontAwesome.Solid.ArrowUp; + icon.Colour = colours.Orange1; + break; + + case RecentActivityType.RankLost: + icon.Icon = FontAwesome.Solid.AngleDoubleDown; + icon.Colour = Color4.White; + break; + + case RecentActivityType.UserSupportAgain: + icon.Icon = FontAwesome.Solid.Heart; + icon.Colour = colours.Pink; + break; + + case RecentActivityType.UserSupportFirst: + icon.Icon = FontAwesome.Solid.Heart; + icon.Colour = colours.Pink; + break; + + case RecentActivityType.UserSupportGift: + icon.Icon = FontAwesome.Solid.Gift; + icon.Colour = colours.Pink; + break; + + case RecentActivityType.UsernameChange: + icon.Icon = FontAwesome.Solid.Tag; + icon.Colour = Color4.White; + break; + } + } + + private Color4 getColorForApprovalType(BeatmapApproval approvalType) + { + switch (approvalType) + { + case BeatmapApproval.Approved: + case BeatmapApproval.Ranked: + return colours.Lime1; + + case BeatmapApproval.Loved: + return colours.Pink1; + + case BeatmapApproval.Qualified: + return colours.Blue1; + + default: + throw new ArgumentOutOfRangeException($"Unsupported {nameof(BeatmapApproval)} type", approvalType, nameof(approvalType)); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index a2e07065f1..7ab10a3c3a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -23,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input public GlobalKeyBindingsSection(GlobalActionContainer manager) { Add(new DefaultBindingsSubsection(manager)); + Add(new OverlayBindingsSubsection(manager)); Add(new AudioControlKeyBindingsSubsection(manager)); Add(new SongSelectKeyBindingSubsection(manager)); Add(new InGameKeyBindingsSubsection(manager)); @@ -40,6 +39,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } + private class OverlayBindingsSubsection : KeyBindingsSubsection + { + protected override LocalisableString Header => InputSettingsStrings.OverlaysSection; + + public OverlayBindingsSubsection(GlobalActionContainer manager) + : base(null) + { + Defaults = manager.OverlayKeyBindings; + } + } + private class SongSelectKeyBindingSubsection : KeyBindingsSubsection { protected override LocalisableString Header => InputSettingsStrings.SongSelectSection; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs index 526c096493..19e6f83dac 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassDeleteConfirmationDialog.cs @@ -1,34 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; -using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; namespace osu.Game.Overlays.Settings.Sections.Maintenance { - public class MassDeleteConfirmationDialog : PopupDialog + public class MassDeleteConfirmationDialog : DeleteConfirmationDialog { public MassDeleteConfirmationDialog(Action deleteAction) { BodyText = "Everything?"; - - Icon = FontAwesome.Regular.TrashAlt; - HeaderText = @"Confirm deletion of"; - Buttons = new PopupDialogButton[] - { - new PopupDialogDangerousButton - { - Text = @"Yes. Go for it.", - Action = deleteAction - }, - new PopupDialogCancelButton - { - Text = @"No! Abort mission!", - }, - }; + DeleteAction = deleteAction; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs index e3870bc76a..fc8c9d497b 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MassVideoDeleteConfirmationDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Game.Overlays.Settings.Sections.Maintenance diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs new file mode 100644 index 0000000000..d35d3ff468 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Game.Database; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Mods; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class ModPresetSettings : SettingsSubsection + { + protected override LocalisableString Header => "Mod presets"; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private INotificationOverlay? notificationOverlay { get; set; } + + private SettingsButton undeleteButton = null!; + private SettingsButton deleteAllButton = null!; + + [BackgroundDependencyLoader] + private void load(IDialogOverlay? dialogOverlay) + { + AddRange(new Drawable[] + { + deleteAllButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllModPresets, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => + { + deleteAllButton.Enabled.Value = false; + Task.Run(deleteAllModPresets).ContinueWith(t => Schedule(onAllModPresetsDeleted, t)); + })); + } + }, + undeleteButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.RestoreAllRecentlyDeletedModPresets, + Action = () => Task.Run(undeleteModPresets).ContinueWith(t => Schedule(onModPresetsUndeleted, t)) + } + }); + } + + private void deleteAllModPresets() => + realm.Write(r => + { + foreach (var preset in r.All()) + preset.DeletePending = true; + }); + + private void onAllModPresetsDeleted(Task deletionTask) + { + deleteAllButton.Enabled.Value = true; + + if (deletionTask.IsCompletedSuccessfully) + notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" }); + else if (deletionTask.IsFaulted) + Logger.Error(deletionTask.Exception, "Failed to delete all mod presets"); + } + + private void undeleteModPresets() => + realm.Write(r => + { + foreach (var preset in r.All().Where(preset => preset.DeletePending)) + preset.DeletePending = false; + }); + + private void onModPresetsUndeleted(Task undeletionTask) + { + undeleteButton.Enabled.Value = true; + + if (undeletionTask.IsCompletedSuccessfully) + notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" }); + else if (undeletionTask.IsFaulted) + Logger.Error(undeletionTask.Exception, "Failed to restore mod presets"); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index 841a7d0abe..4be6d1653b 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -25,7 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections new BeatmapSettings(), new SkinSettings(), new CollectionsSettings(), - new ScoreSettings() + new ScoreSettings(), + new ModPresetSettings() }; } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index a93ba17c5b..4ebd19a1f7 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -1,17 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Resources.Localisation.Web; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; @@ -20,59 +22,103 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarUserButton : ToolbarOverlayToggleButton { - private readonly UpdateableAvatar avatar; + private UpdateableAvatar avatar = null!; - [Resolved] - private IAPIProvider api { get; set; } + private IBindable localUser = null!; - private readonly IBindable apiState = new Bindable(); + private LoadingSpinner spinner = null!; + + private SpriteIcon failingIcon = null!; + + private IBindable apiState = null!; public ToolbarUserButton() { AutoSizeAxes = Axes.X; + } - DrawableText.Font = OsuFont.GetFont(italics: true); - + [BackgroundDependencyLoader] + private void load(OsuColour colours, IAPIProvider api, LoginOverlay? login) + { Add(new OpaqueBackground { Depth = 1 }); - Flow.Add(avatar = new UpdateableAvatar(isInteractive: false) + Flow.Add(new Container { Masking = true, + CornerRadius = 4, Size = new Vector2(32), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - CornerRadius = 4, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Radius = 4, Colour = Color4.Black.Opacity(0.1f), + }, + Children = new Drawable[] + { + avatar = new UpdateableAvatar(isInteractive: false) + { + RelativeSizeAxes = Axes.Both, + }, + spinner = new LoadingLayer(dimBackground: true, withBox: false, blockInput: false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + failingIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Size = new Vector2(0.3f), + Icon = FontAwesome.Solid.ExclamationTriangle, + RelativeSizeAxes = Axes.Both, + Colour = colours.YellowLight, + }, } }); - } - [BackgroundDependencyLoader(true)] - private void load(LoginOverlay login) - { - apiState.BindTo(api.State); + apiState = api.State.GetBoundCopy(); apiState.BindValueChanged(onlineStateChanged, true); + localUser = api.LocalUser.GetBoundCopy(); + localUser.BindValueChanged(userChanged, true); + StateContainer = login; } + private void userChanged(ValueChangedEvent user) => Schedule(() => + { + Text = user.NewValue.Username; + avatar.User = user.NewValue; + }); + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { + failingIcon.FadeTo(state.NewValue == APIState.Failing ? 1 : 0, 200, Easing.OutQuint); + switch (state.NewValue) { - default: - Text = UsersStrings.AnonymousUsername; - avatar.User = new APIUser(); + case APIState.Connecting: + TooltipText = ToolbarStrings.Connecting; + spinner.Show(); break; - case APIState.Online: - Text = api.LocalUser.Value.Username; - avatar.User = api.LocalUser.Value; + case APIState.Failing: + TooltipText = ToolbarStrings.AttemptingToReconnect; + spinner.Show(); break; + + case APIState.Offline: + case APIState.Online: + TooltipText = string.Empty; + spinner.Hide(); + break; + + default: + throw new ArgumentOutOfRangeException(); } }); } diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs index 93fb7b9ab8..a66c00d78b 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; +using osu.Game.Beatmaps; using osu.Game.IO.FileAbstraction; using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Storyboards; diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs index b15a9f5331..be0c4501e7 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit.Checks.Components; namespace osu.Game.Rulesets.Edit.Checks diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 38d26ed05a..7b84db844b 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Fun; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.5; public override bool ValidForMultiplayer => false; public override bool ValidForMultiplayerAsFreeMod => false; diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index eefa1531c4..b7435ec3ec 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => FontAwesome.Solid.Hammer; - public override double ScoreMultiplier => 1.0; + public override double ScoreMultiplier => 0.5; public override bool RequiresConfiguration => true; diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index e8bc6c2026..210dd56137 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -6,12 +6,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Graphics; @@ -211,17 +210,12 @@ namespace osu.Game.Rulesets.Mods private Vector2 flashlightSize; private float flashlightDim; - private readonly VertexBatch quadBatch = new QuadBatch(1, 1); - private readonly Action addAction; + private IVertexBatch? quadBatch; + private Action? addAction; public FlashlightDrawNode(Flashlight source) : base(source) { - addAction = v => quadBatch.Add(new PositionAndColourVertex - { - Position = v.Position, - Colour = v.Colour - }); } public override void ApplyState() @@ -235,9 +229,19 @@ namespace osu.Game.Rulesets.Mods flashlightDim = Source.FlashlightDim; } - public override void Draw(Action vertexAction) + public override void Draw(IRenderer renderer) { - base.Draw(vertexAction); + base.Draw(renderer); + + if (quadBatch == null) + { + quadBatch = renderer.CreateQuadBatch(1, 1); + addAction = v => quadBatch.Add(new PositionAndColourVertex + { + Position = v.Position, + Colour = v.Colour + }); + } shader.Bind(); @@ -245,7 +249,7 @@ namespace osu.Game.Rulesets.Mods shader.GetUniform("flashlightSize").UpdateValue(ref flashlightSize); shader.GetUniform("flashlightDim").UpdateValue(ref flashlightDim); - DrawQuad(Texture.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction); + renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction); shader.Unbind(); } @@ -253,7 +257,7 @@ namespace osu.Game.Rulesets.Mods protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - quadBatch.Dispose(); + quadBatch?.Dispose(); } } } diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index e5995ff180..49c10339ee 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string Acronym => "RX"; public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; - public override double ScoreMultiplier => 1; + public override double ScoreMultiplier => 0.1; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 59fee0f97b..1deac9f08a 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -16,6 +16,8 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Scoring; +using osu.Framework.Localisation; +using osu.Game.Localisation; namespace osu.Game.Rulesets.Scoring { @@ -636,7 +638,10 @@ namespace osu.Game.Rulesets.Scoring public enum ScoringMode { + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.StandardisedScoreDisplay))] Standardised, + + [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.ClassicScoreDisplay))] Classic } } diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 6a7369a150..cffbf2c9a1 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -12,7 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; @@ -55,15 +55,17 @@ namespace osu.Game.Rulesets.UI if (resources != null) { - TextureStore = new TextureStore(parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); - CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); + var host = parent.Get(); + + TextureStore = new TextureStore(host.Renderer, parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + CacheAs(TextureStore = new FallbackTextureStore(host.Renderer, TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get())); - ShaderManager = new ShaderManager(new NamespacedResourceStore(resources, @"Shaders")); - CacheAs(ShaderManager = new FallbackShaderManager(ShaderManager, parent.Get())); + ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders")); + CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get())); } RulesetConfigManager = parent.Get().GetConfigFor(ruleset); @@ -170,7 +172,8 @@ namespace osu.Game.Rulesets.UI private readonly TextureStore primary; private readonly TextureStore fallback; - public FallbackTextureStore(TextureStore primary, TextureStore fallback) + public FallbackTextureStore(IRenderer renderer, TextureStore primary, TextureStore fallback) + : base(renderer) { this.primary = primary; this.fallback = fallback; @@ -191,8 +194,8 @@ namespace osu.Game.Rulesets.UI private readonly ShaderManager primary; private readonly ShaderManager fallback; - public FallbackShaderManager(ShaderManager primary, ShaderManager fallback) - : base(new ResourceStore()) + public FallbackShaderManager(IRenderer renderer, ShaderManager primary, ShaderManager fallback) + : base(renderer, new ResourceStore()) { this.primary = primary; this.fallback = fallback; diff --git a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs index a4a483e930..79f3a2ca84 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs @@ -3,12 +3,20 @@ #nullable disable +using osu.Framework.Localisation; +using osu.Game.Localisation; + namespace osu.Game.Rulesets.UI { public enum PlayfieldBorderStyle { + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderNone))] None, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderCorners))] Corners, + + [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.BorderFull))] Full } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9e9cd8e3b7..89f9aec5ee 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -10,6 +10,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -949,7 +950,7 @@ namespace osu.Game.Screens.Edit ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => clock; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; private class BeatmapEditorToast : Toast { diff --git a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs index 475c894b30..a106a60379 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Skinning; diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 8c14feebbc..69953bf659 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -4,7 +4,6 @@ #nullable disable using System.IO; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -78,7 +77,7 @@ namespace osu.Game.Screens.Edit.Setup // remove the previous background for now. // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.BackgroundFile); + var oldFile = set.GetFile(working.Value.Metadata.BackgroundFile); using (var stream = source.OpenRead()) { @@ -107,7 +106,7 @@ namespace osu.Game.Screens.Edit.Setup // remove the previous audio track for now. // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.AudioFile); + var oldFile = set.GetFile(working.Value.Metadata.AudioFile); using (var stream = source.OpenRead()) { diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index c66bd3639a..c7bda4d8f8 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -1,27 +1,23 @@ // Copyright (c) ppy Pty Ltd . 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.Graphics; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; using System; using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; -using osu.Framework.Bindables; -using osu.Framework.Utils; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Primitives; +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; namespace osu.Game.Screens.Menu { @@ -30,8 +26,6 @@ namespace osu.Game.Screens.Menu /// public class LogoVisualisation : Drawable { - private readonly IBindable beatmap = new Bindable(); - /// /// The number of bars to jump each update iteration. /// @@ -76,12 +70,11 @@ namespace osu.Game.Screens.Menu private readonly float[] frequencyAmplitudes = new float[256]; - private IShader shader; - private readonly Texture texture; + private IShader shader = null!; + private Texture texture = null!; public LogoVisualisation() { - texture = Texture.WhitePixel; Blending = BlendingParameters.Additive; } @@ -93,32 +86,31 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(ShaderManager shaders, IBindable beatmap) + private void load(IRenderer renderer, ShaderManager shaders) { - this.beatmap.BindTo(beatmap); + texture = renderer.WhitePixel; shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); } private readonly float[] temporalAmplitudes = new float[ChannelAmplitudes.AMPLITUDES_SIZE]; + [Resolved] + private IBeatSyncProvider beatSyncProvider { get; set; } = null!; + private void updateAmplitudes() { - var effect = beatmap.Value.BeatmapLoaded && beatmap.Value.TrackLoaded - ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(beatmap.Value.Track.CurrentTime) - : null; - for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (beatmap.Value.TrackLoaded) - addAmplitudesFromSource(beatmap.Value.Track); + if (beatSyncProvider.Clock != null) + addAmplitudesFromSource(beatSyncProvider); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); for (int i = 0; i < bars_per_visualiser; i++) { - float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (effect?.KiaiMode == true ? 1 : 0.5f); + float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (beatSyncProvider.CheckIsKiaiTime() ? 1 : 0.5f); if (targetAmplitude > frequencyAmplitudes[i]) frequencyAmplitudes[i] = targetAmplitude; } @@ -153,7 +145,7 @@ namespace osu.Game.Screens.Menu protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); - private void addAmplitudesFromSource([NotNull] IHasAmplitudes source) + private void addAmplitudesFromSource(IHasAmplitudes source) { if (source == null) throw new ArgumentNullException(nameof(source)); @@ -170,8 +162,8 @@ namespace osu.Game.Screens.Menu { protected new LogoVisualisation Source => (LogoVisualisation)base.Source; - private IShader shader; - private Texture texture; + private IShader shader = null!; + private Texture texture = null!; // Assuming the logo is a circle, we don't need a second dimension. private float size; @@ -180,7 +172,7 @@ namespace osu.Game.Screens.Menu private readonly float[] audioData = new float[256]; - private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); + private IVertexBatch? vertexBatch; public VisualisationDrawNode(LogoVisualisation source) : base(source) @@ -198,9 +190,11 @@ namespace osu.Game.Screens.Menu Source.frequencyAmplitudes.AsSpan().CopyTo(audioData); } - public override void Draw(Action vertexAction) + public override void Draw(IRenderer renderer) { - base.Draw(vertexAction); + base.Draw(renderer); + + vertexBatch ??= renderer.CreateQuadBatch(100, 10); shader.Bind(); @@ -209,43 +203,40 @@ namespace osu.Game.Screens.Menu ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(transparent_white); - if (audioData != null) + for (int j = 0; j < visualiser_rounds; j++) { - for (int j = 0; j < visualiser_rounds; j++) + for (int i = 0; i < bars_per_visualiser; i++) { - for (int i = 0; i < bars_per_visualiser; i++) - { - if (audioData[i] < amplitude_dead_zone) - continue; + if (audioData[i] < amplitude_dead_zone) + continue; - float rotation = MathUtils.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; + float rotation = MathUtils.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]); - // 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. - var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); + var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.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. + var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); - var rectangle = new Quad( - Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) - ); + var rectangle = new Quad( + Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) + ); - DrawQuad( - texture, - rectangle, - colourInfo, - null, - vertexBatch.AddAction, - // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. - Vector2.Divide(inflation, barSize.Yx)); - } + renderer.DrawQuad( + texture, + rectangle, + colourInfo, + null, + vertexBatch.AddAction, + // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. + Vector2.Divide(inflation, barSize.Yx)); } } @@ -256,7 +247,7 @@ namespace osu.Game.Screens.Menu { base.Dispose(isDisposing); - vertexBatch.Dispose(); + vertexBatch?.Dispose(); } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 0909f615f2..e9b50f94f7 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Menu float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); - triangles.Velocity = (float)Interpolation.Damp(triangles.Velocity, triangles_paused_velocity * (LastEffectPoint.KiaiMode ? 4 : 2), 0.995f, Time.Elapsed); + triangles.Velocity = (float)Interpolation.Damp(triangles.Velocity, triangles_paused_velocity * (IsKiaiTime ? 4 : 2), 0.995f, Time.Elapsed); } else { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 8dccc3d82f..beecd56b52 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay private IBeatmapInfo beatmap; private IRulesetInfo ruleset; - private Mod[] requiredMods; + private Mod[] requiredMods = Array.Empty(); private Container maskingContainer; private Container difficultyIconContainer; @@ -139,7 +139,8 @@ namespace osu.Game.Screens.OnlinePlay ruleset = rulesets.GetRuleset(Item.RulesetID); var rulesetInstance = ruleset?.CreateInstance(); - requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + if (rulesetInstance != null) + requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 8270bb3b6f..5cd9e0ddf9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -231,6 +231,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed) return; + // we could also potentially receive EndGameplay with "Playing" state, at which point we can only early-return and hope it's a passing player. + // todo: this shouldn't exist, but it's here as a hotfix for an issue with multi-spectator screen not proceeding to results screen. + // see: https://github.com/ppy/osu/issues/19593 + if (state.State == SpectatedUserState.Playing) + return; + RemoveUser(userId); var instance = instances.Single(i => i.UserId == userId); diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 96a6c56860..659984682e 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -40,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD public override bool HandleNonPositionalInput => AllowSeeking.Value; public override bool HandlePositionalInput => AllowSeeking.Value; - protected override bool BlockScrollInput => false; - [Resolved] private Player? player { get; set; } diff --git a/osu.Game/Screens/Play/HUD/SongProgress.cs b/osu.Game/Screens/Play/HUD/SongProgress.cs index 702d2f7c6f..09afd7a9d3 100644 --- a/osu.Game/Screens/Play/HUD/SongProgress.cs +++ b/osu.Game/Screens/Play/HUD/SongProgress.cs @@ -14,6 +14,12 @@ namespace osu.Game.Screens.Play.HUD { public abstract class SongProgress : OverlayContainer, ISkinnableDrawable { + // Some implementations of this element allow seeking during gameplay playback. + // Set a sane default of never handling input to override the behaviour provided by OverlayContainer. + public override bool HandleNonPositionalInput => false; + public override bool HandlePositionalInput => false; + protected override bool BlockScrollInput => false; + public bool UsesFixedAnchor { get; set; } [Resolved] diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index cd37c541ec..d7f6992fee 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -53,21 +51,21 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; - private HardwareCorrectionOffsetClock userGlobalOffsetClock; - private HardwareCorrectionOffsetClock userBeatmapOffsetClock; - private HardwareCorrectionOffsetClock platformOffsetClock; - private MasterGameplayClock masterGameplayClock; - private Bindable userAudioOffset; + private HardwareCorrectionOffsetClock userGlobalOffsetClock = null!; + private HardwareCorrectionOffsetClock userBeatmapOffsetClock = null!; + private HardwareCorrectionOffsetClock platformOffsetClock = null!; + private MasterGameplayClock masterGameplayClock = null!; + private Bindable userAudioOffset = null!; - private IDisposable beatmapOffsetSubscription; + private IDisposable? beatmapOffsetSubscription; private readonly double skipTargetTime; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; [Resolved] - private OsuConfigManager config { get; set; } + private OsuConfigManager config { get; set; } = null!; /// /// Create a new master gameplay clock container. @@ -255,7 +253,7 @@ namespace osu.Game.Screens.Play ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => GameplayClock; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : null; + ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty; private class HardwareCorrectionOffsetClock : FramedOffsetClock { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9c08c77d91..08b6da1921 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -249,6 +249,9 @@ namespace osu.Game.Screens.Play // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. GameplayClockContainer.Add(rulesetSkinProvider); + if (cancellationToken.IsCancellationRequested) + return; + rulesetSkinProvider.AddRange(new Drawable[] { failAnimationLayer = new FailAnimation(DrawableRuleset) @@ -279,6 +282,9 @@ namespace osu.Game.Screens.Play }, }); + if (cancellationToken.IsCancellationRequested) + return; + if (Configuration.AllowRestart) { rulesetSkinProvider.Add(new HotkeyRetryOverlay diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 84426d230b..0996e3e202 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -1,43 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; using osu.Game.Scoring; namespace osu.Game.Screens.Select { - public class BeatmapClearScoresDialog : PopupDialog + public class BeatmapClearScoresDialog : DeleteConfirmationDialog { [Resolved] - private ScoreManager scoreManager { get; set; } + private ScoreManager scoreManager { get; set; } = null!; public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { - BodyText = beatmapInfo.GetDisplayTitle(); - Icon = FontAwesome.Solid.Eraser; - HeaderText = @"Clearing all local scores. Are you sure?"; - Buttons = new PopupDialogButton[] + BodyText = $"All local scores on {beatmapInfo.GetDisplayTitle()}"; + DeleteAction = () => { - new PopupDialogOkButton - { - Text = @"Yes. Please.", - Action = () => - { - Task.Run(() => scoreManager.Delete(beatmapInfo)) - .ContinueWith(_ => onCompletion); - } - }, - new PopupDialogCancelButton - { - Text = @"No, I'm still attached.", - }, + Task.Run(() => scoreManager.Delete(beatmapInfo)) + .ContinueWith(_ => onCompletion); }; } } diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index e3b83d5780..3d3e8b6d73 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -1,43 +1,26 @@ // Copyright (c) ppy Pty Ltd . 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.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public class BeatmapDeleteDialog : PopupDialog + public class BeatmapDeleteDialog : DeleteConfirmationDialog { - private BeatmapManager manager; + private readonly BeatmapSetInfo beatmapSet; + + public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet) + { + this.beatmapSet = beatmapSet; + BodyText = $@"{beatmapSet.Metadata.Artist} - {beatmapSet.Metadata.Title}"; + } [BackgroundDependencyLoader] private void load(BeatmapManager beatmapManager) { - manager = beatmapManager; - } - - public BeatmapDeleteDialog(BeatmapSetInfo beatmap) - { - BodyText = $@"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}"; - - Icon = FontAwesome.Regular.TrashAlt; - HeaderText = @"Confirm deletion of"; - Buttons = new PopupDialogButton[] - { - new PopupDialogDangerousButton - { - Text = @"Yes. Totally. Delete it.", - Action = () => manager?.Delete(beatmap), - }, - new PopupDialogCancelButton - { - Text = @"Firetruck, I didn't mean to!", - }, - }; + DeleteAction = () => beatmapManager.Delete(beatmapSet); } } } diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 3c05a1fa5a..d9312679bc 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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.Game.Overlays.Dialog; using osu.Game.Scoring; @@ -12,44 +10,25 @@ using osu.Game.Beatmaps; namespace osu.Game.Screens.Select { - public class LocalScoreDeleteDialog : PopupDialog + public class LocalScoreDeleteDialog : DeleteConfirmationDialog { private readonly ScoreInfo score; - [Resolved] - private ScoreManager scoreManager { get; set; } - - [Resolved] - private BeatmapManager beatmapManager { get; set; } - public LocalScoreDeleteDialog(ScoreInfo score) { this.score = score; - Debug.Assert(score != null); } [BackgroundDependencyLoader] - private void load() + private void load(BeatmapManager beatmapManager, ScoreManager scoreManager) { - BeatmapInfo beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + BeatmapInfo? beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); Debug.Assert(beatmapInfo != null); BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; Icon = FontAwesome.Regular.TrashAlt; - HeaderText = "Confirm deletion of local score"; - Buttons = new PopupDialogButton[] - { - new PopupDialogDangerousButton - { - Text = "Yes. Please.", - Action = () => scoreManager?.Delete(score) - }, - new PopupDialogCancelButton - { - Text = "No, I'm still attached.", - }, - }; + DeleteAction = () => scoreManager.Delete(score); } } } diff --git a/osu.Game/Screens/Select/SkinDeleteDialog.cs b/osu.Game/Screens/Select/SkinDeleteDialog.cs index 8defd6347a..c701d9049f 100644 --- a/osu.Game/Screens/Select/SkinDeleteDialog.cs +++ b/osu.Game/Screens/Select/SkinDeleteDialog.cs @@ -1,43 +1,29 @@ // Copyright (c) ppy Pty Ltd . 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.Graphics.Sprites; using osu.Game.Skinning; using osu.Game.Overlays.Dialog; namespace osu.Game.Screens.Select { - public class SkinDeleteDialog : PopupDialog + public class SkinDeleteDialog : DeleteConfirmationDialog { - [Resolved] - private SkinManager manager { get; set; } + private readonly Skin skin; public SkinDeleteDialog(Skin skin) { + this.skin = skin; BodyText = skin.SkinInfo.Value.Name; - Icon = FontAwesome.Regular.TrashAlt; - HeaderText = @"Confirm deletion of"; - Buttons = new PopupDialogButton[] - { - new PopupDialogDangerousButton - { - Text = @"Yes. Totally. Delete it.", - Action = () => - { - if (manager == null) - return; + } - manager.Delete(skin.SkinInfo.Value); - manager.CurrentSkinInfo.SetDefault(); - }, - }, - new PopupDialogCancelButton - { - Text = @"Firetruck, I didn't mean to!", - }, + [BackgroundDependencyLoader] + private void load(SkinManager manager) + { + DeleteAction = () => + { + manager.Delete(skin.SkinInfo.Value); + manager.CurrentSkinInfo.SetDefault(); }; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 596a8eb896..33ff31857f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -313,7 +313,7 @@ namespace osu.Game.Screens.Select (new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { @@ -927,5 +927,10 @@ namespace osu.Game.Screens.Select return base.OnHover(e); } } + + private class SoloModSelectOverlay : UserModSelectOverlay + { + protected override bool ShowPresets => true; + } } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 7d217dd956..f10e8412b1 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -54,7 +53,7 @@ namespace osu.Game.Skinning { foreach (string lookup in sampleInfo.LookupNames) { - var sample = Samples?.Get(lookup) ?? resources.AudioManager.Samples.Get(lookup); + var sample = Samples?.Get(lookup) ?? resources.AudioManager?.Samples.Get(lookup); if (sample != null) return sample; } diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index dcb9e9bbb9..a65d15d24b 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; using osu.Framework.Timing; namespace osu.Game.Skinning diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 431fc1c0b8..c093dc2f32 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -4,7 +4,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 15d4965a1d..1e096702b3 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 5a537dd57d..8765882af2 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using static osu.Game.Skinning.SkinConfiguration; diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index da7da759fc..8f2526db37 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -8,7 +8,6 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; diff --git a/osu.Game/Skinning/ResourceStoreBackedSkin.cs b/osu.Game/Skinning/ResourceStoreBackedSkin.cs index 597eef99d4..efdbb0a207 100644 --- a/osu.Game/Skinning/ResourceStoreBackedSkin.cs +++ b/osu.Game/Skinning/ResourceStoreBackedSkin.cs @@ -6,7 +6,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; @@ -24,7 +23,7 @@ namespace osu.Game.Skinning public ResourceStoreBackedSkin(IResourceStore resources, GameHost host, AudioManager audio) { - textures = new TextureStore(host.CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + textures = new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); samples = audio.GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 7d93aeb897..dd43e4dc54 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -11,7 +11,6 @@ using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -69,14 +68,16 @@ namespace osu.Game.Skinning storage ??= realmBackedStorage = new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess); - (storage as ResourceStore)?.AddExtension("ogg"); - var samples = resources.AudioManager?.GetSampleStore(storage); if (samples != null) samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + // osu-stable performs audio lookups in order of wav -> mp3 -> ogg. + // The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering. + (storage as ResourceStore)?.AddExtension("ogg"); + Samples = samples; - Textures = new TextureStore(resources.CreateTextureLoaderStore(storage)); + Textures = new TextureStore(resources.Renderer, resources.CreateTextureLoaderStore(storage)); } else { diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index ed31a8c3f5..f270abd163 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -10,6 +10,7 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; @@ -49,7 +50,7 @@ namespace osu.Game.Skinning protected override void Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { - var skinInfoFile = model.Files.SingleOrDefault(f => f.Filename == skin_info_file); + var skinInfoFile = model.GetFile(skin_info_file); if (skinInfoFile != null) { @@ -129,7 +130,7 @@ namespace osu.Game.Skinning authorLine, }; - var existingFile = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)); + var existingFile = item.GetFile(@"skin.ini"); if (existingFile == null) { @@ -163,7 +164,7 @@ namespace osu.Game.Skinning { Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important); - var existingIni = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)); + var existingIni = item.GetFile(@"skin.ini"); if (existingIni != null) item.Files.Remove(existingIni); @@ -248,7 +249,7 @@ namespace osu.Game.Skinning { string filename = @$"{drawableInfo.Key}.json"; - var oldFile = s.Files.FirstOrDefault(f => f.Filename == filename); + var oldFile = s.GetFile(filename); if (oldFile != null) modelManager.ReplaceFile(oldFile, streamContent, s.Realm); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index dd35f83434..f677cebe51 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -14,7 +14,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; @@ -249,6 +249,7 @@ namespace osu.Game.Skinning #region IResourceStorageProvider + IRenderer IStorageResourceProvider.Renderer => host.Renderer; AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Resources => resources; IResourceStore IStorageResourceProvider.Files => userFiles; diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index e447570931..42c39e581f 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index be98ca9a46..8343f14050 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -90,7 +90,7 @@ namespace osu.Game.Storyboards.Drawables if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new RealmFileStore(realm, host.Storage).Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 23b1004e74..ebd056ba50 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -1,10 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,14 +8,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; -using osu.Game.Extensions; namespace osu.Game.Storyboards.Drawables { public class DrawableStoryboardVideo : CompositeDrawable { public readonly StoryboardVideo Video; - private Video video; + + private Video? drawableVideo; public override bool RemoveWhenNotAlive => false; @@ -33,7 +29,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader(true)] private void load(IBindable beatmap, TextureStore textureStore) { - string path = beatmap.Value.BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.Equals(Video.Path, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); + string? path = beatmap.Value.BeatmapSetInfo?.GetPathForFile(Video.Path); if (path == null) return; @@ -43,7 +39,7 @@ namespace osu.Game.Storyboards.Drawables if (stream == null) return; - InternalChild = video = new Video(stream, false) + InternalChild = drawableVideo = new Video(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, @@ -57,12 +53,12 @@ namespace osu.Game.Storyboards.Drawables { base.LoadComplete(); - if (video == null) return; + if (drawableVideo == null) return; - using (video.BeginAbsoluteSequence(Video.StartTime)) + using (drawableVideo.BeginAbsoluteSequence(Video.StartTime)) { - Schedule(() => video.PlaybackPosition = Time.Current - Video.StartTime); - video.FadeIn(500); + Schedule(() => drawableVideo.PlaybackPosition = Time.Current - Video.StartTime); + drawableVideo.FadeIn(500); } } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index f30c3f7f94..473f1ce97f 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -1,14 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -using osu.Game.Extensions; using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; @@ -90,12 +86,12 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) => + public DrawableStoryboard CreateDrawable(IReadOnlyList? mods = null) => new DrawableStoryboard(this, mods); - public Texture GetTextureFromPath(string path, TextureStore textureStore) + public Texture? GetTextureFromPath(string path, TextureStore textureStore) { - string storyboardPath = BeatmapInfo.BeatmapSet?.Files.FirstOrDefault(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath(); + string? storyboardPath = BeatmapInfo.BeatmapSet?.GetPathForFile(path); if (!string.IsNullOrEmpty(storyboardPath)) return textureStore.Get(storyboardPath); diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 3ed3bb65c9..c97eec116c 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -12,8 +12,10 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -39,6 +41,9 @@ namespace osu.Game.Tests.Beatmaps [Resolved] private RulesetStore rulesetStore { get; set; } + [Resolved] + private GameHost host { get; set; } + private readonly SkinInfo userSkinInfo = new SkinInfo(); private readonly BeatmapInfo beatmapInfo = new BeatmapInfo @@ -123,6 +128,7 @@ namespace osu.Game.Tests.Beatmaps #region IResourceStorageProvider + public IRenderer Renderer => host.Renderer; public AudioManager AudioManager => Audio; public IResourceStore Files => userSkinResourceStore; public new IResourceStore Resources => base.Resources; @@ -212,6 +218,7 @@ namespace osu.Game.Tests.Beatmaps protected internal override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, this); + public IRenderer Renderer => resources.Renderer; public AudioManager AudioManager => resources.AudioManager; public IResourceStore Files { get; } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 2e5d0534f6..ffdde782a5 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -11,7 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; @@ -159,6 +159,7 @@ namespace osu.Game.Tests.Visual #region IResourceStorageProvider + public IRenderer Renderer => host.Renderer; public AudioManager AudioManager => Audio; public IResourceStore Files => null; public new IResourceStore Resources => base.Resources; diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index e2ddd1734d..93a155e083 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Online.Spectator; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -46,6 +47,9 @@ namespace osu.Game.Tests.Visual public readonly List Results = new List(); + [Resolved] + private SpectatorClient spectatorClient { get; set; } + public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(new PlayerConfiguration { @@ -98,5 +102,15 @@ namespace osu.Game.Tests.Visual ScoreProcessor.NewJudgement += r => Results.Add(r); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // Specific to tests, the player can be disposed without OnExiting() ever being called. + // We should make sure that the gameplay session has finished even in this case. + if (LoadedBeatmapSuccessfully) + spectatorClient?.EndPlaying(GameplayState); + } } } diff --git a/osu.Game/Tests/VisualTestRunner.cs b/osu.Game/Tests/VisualTestRunner.cs index bd98482768..c8279b9e3c 100644 --- a/osu.Game/Tests/VisualTestRunner.cs +++ b/osu.Game/Tests/VisualTestRunner.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests [STAThread] public static int Main(string[] args) { - using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu", new HostOptions { BindIPC = true, })) + using (DesktopGameHost host = Host.GetSuitableDesktopHost(@"osu-development", new HostOptions { BindIPC = true, })) { host.Run(new OsuTestBrowser()); return 0; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ecf5972797..c17d45e84a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,8 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 74d8f0d471..5bfb53bc9d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ - - + + @@ -84,7 +84,7 @@ - +