diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 39fa0f9f6c..8be6479043 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,4 +1,2 @@ # Normalize all the line endings 32a74f95a5c80a0ed18e693f13a47522099df5c3 -# Enabled NRT globally -f8830c6850128456266c82de83273204f8b74ac0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef729a779f..c728d89ed1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,12 @@ jobs: with: dotnet-version: "6.0.x" + # macOS agents recently have empty NuGet config files, resulting in restore failures, + # see https://github.com/actions/virtual-environments/issues/5768 + # Add the global nuget package source manually for now. + - name: Setup NuGet.Config + run: echo '' > ~/.config/NuGet/NuGet.Config + # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono # cannot accept .sln(f) files as arguments. # Build just the main game for now. diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index b72df0a306..8b5431e2d6 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -16,3 +16,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList,NotificationCallbackDelegate) instead. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. +M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. +M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. +M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. diff --git a/Directory.Build.props b/Directory.Build.props index dbc84fb88f..235feea8ce 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@  - 8.0 + 9.0 true enable diff --git a/Gemfile.lock b/Gemfile.lock index ddab497657..cae682ec2b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,20 +8,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.570.0) - aws-sdk-core (3.130.0) + aws-partitions (1.601.0) + aws-sdk-core (3.131.2) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.55.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.0) + aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.4.0) + aws-sigv4 (1.5.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -36,7 +36,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) emoji_regex (3.2.3) - excon (0.92.1) + excon (0.92.3) faraday (1.10.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -56,8 +56,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -66,7 +66,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.205.1) + fastlane (2.206.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -110,9 +110,9 @@ GEM souyuz (= 0.11.1) fastlane-plugin-xamarin (0.6.3) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.16.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-androidpublisher_v3 (0.23.0) + google-apis-core (>= 0.6, < 2.a) + google-apis-core (0.6.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -121,19 +121,19 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.10.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.7.0) - google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.11.0) - google-apis-core (>= 0.4, < 2.a) + google-apis-iamcredentials_v1 (0.12.0) + google-apis-core (>= 0.6, < 2.a) + google-apis-playcustomapp_v1 (0.9.0) + google-apis-core (>= 0.6, < 2.a) + google-apis-storage_v1 (0.16.0) + google-apis-core (>= 0.6, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.36.1) + google-cloud-storage (1.36.2) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) @@ -141,7 +141,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.1.2) + googleauth (1.2.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -149,12 +149,12 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.1) - json (2.6.1) - jwt (2.3.0) + json (2.6.2) + jwt (2.4.1) memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) @@ -169,10 +169,10 @@ GEM optparse (0.1.1) os (1.1.4) plist (3.6.0) - public_suffix (4.0.6) + public_suffix (4.0.7) racc (1.6.0) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -182,9 +182,9 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.16.1) + signet (0.17.0) addressable (~> 2.8) - faraday (>= 0.17.5, < 3.0) + faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) simctl (1.6.8) @@ -205,11 +205,11 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8.1) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/osu.Android.props b/osu.Android.props index 8fcd7ef8c0..2ef3bb2dd1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Android/AndroidMouseSettings.cs b/osu.Android/AndroidMouseSettings.cs new file mode 100644 index 0000000000..54b787fd17 --- /dev/null +++ b/osu.Android/AndroidMouseSettings.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Android.OS; +using osu.Framework.Allocation; +using osu.Framework.Android.Input; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Input; + +namespace osu.Android +{ + public class AndroidMouseSettings : SettingsSubsection + { + private readonly AndroidMouseHandler mouseHandler; + + protected override LocalisableString Header => MouseSettingsStrings.Mouse; + + private Bindable handlerSensitivity = null!; + + private Bindable localSensitivity = null!; + + private Bindable relativeMode = null!; + + public AndroidMouseSettings(AndroidMouseHandler mouseHandler) + { + this.mouseHandler = mouseHandler; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager osuConfig) + { + // use local bindable to avoid changing enabled state of game host's bindable. + handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy(); + localSensitivity = handlerSensitivity.GetUnboundCopy(); + + relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); + + // High precision/pointer capture is only available on Android 8.0 and up + if (Build.VERSION.SdkInt >= BuildVersionCodes.O) + { + AddRange(new Drawable[] + { + new SettingsCheckbox + { + LabelText = MouseSettingsStrings.HighPrecisionMouse, + TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip, + Current = relativeMode, + Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" }, + }, + new MouseSettings.SensitivitySetting + { + LabelText = MouseSettingsStrings.CursorSensitivity, + Current = localSensitivity, + }, + }); + } + + AddRange(new Drawable[] + { + new SettingsCheckbox + { + LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust, + TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip, + Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel), + }, + new SettingsCheckbox + { + LabelText = MouseSettingsStrings.DisableMouseButtons, + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons), + }, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true); + + handlerSensitivity.BindValueChanged(val => + { + bool disabled = localSensitivity.Disabled; + + localSensitivity.Disabled = false; + localSensitivity.Value = val.NewValue; + localSensitivity.Disabled = disabled; + }, true); + + localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); + } + } +} diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 1edb867e05..636fc7d2df 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -7,7 +7,11 @@ using System; using Android.App; using Android.OS; using osu.Framework.Allocation; +using osu.Framework.Android.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.Platform; using osu.Game; +using osu.Game.Overlays.Settings; using osu.Game.Updater; using osu.Game.Utils; using Xamarin.Essentials; @@ -75,10 +79,28 @@ namespace osu.Android LoadComponentAsync(new GameplayScreenRotationLocker(), Add); } + public override void SetHost(GameHost host) + { + base.SetHost(host); + host.Window.CursorState |= CursorState.Hidden; + } + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); + public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler) + { + switch (handler) + { + case AndroidMouseHandler mh: + return new AndroidMouseSettings(mh); + + default: + return base.CreateSettingsSubsectionFor(handler); + } + } + private class AndroidBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel; diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index fc50ca9fa1..90b02c527b 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -26,6 +26,7 @@ true + diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f2531c1cae..d0b6953c30 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -57,7 +57,7 @@ namespace osu.Desktop client.OnReady += onReady; // safety measure for now, until we performance test / improve backoff for failed connections. - client.OnConnectionFailed += (_, __) => client.Deinitialize(); + client.OnConnectionFailed += (_, _) => client.Deinitialize(); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 6713136343..314a03a73e 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.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.Diagnostics; @@ -20,25 +18,34 @@ using osu.Framework; using osu.Framework.Logging; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Joystick; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Threading; using osu.Game.IO; +using osu.Game.IPC; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Input; namespace osu.Desktop { internal class OsuGameDesktop : OsuGame { - public OsuGameDesktop(string[] args = null) + private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel; + + public OsuGameDesktop(string[]? args = null) : base(args) { } - public override StableStorage GetStorageForStableInstall() + public override StableStorage? GetStorageForStableInstall() { try { if (Host is DesktopGameHost desktopHost) { - string stablePath = getStableInstallPath(); + string? stablePath = getStableInstallPath(); if (!string.IsNullOrEmpty(stablePath)) return new StableStorage(stablePath, desktopHost); } @@ -51,11 +58,11 @@ namespace osu.Desktop return null; } - private string getStableInstallPath() + private string? getStableInstallPath() { static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg")); - string stableInstallPath; + string? stableInstallPath; if (OperatingSystem.IsWindows()) { @@ -83,15 +90,15 @@ namespace osu.Desktop } [SupportedOSPlatform("windows")] - private string getStableInstallPathFromRegistry() + private string? getStableInstallPathFromRegistry() { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu")) return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); } protected override UpdateManager CreateUpdateManager() { - string packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); + string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); @@ -118,6 +125,8 @@ namespace osu.Desktop LoadComponentAsync(new GameplayWinKeyBlocker(), Add); LoadComponentAsync(new ElevatedPrivilegesChecker(), Add); + + osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this); } public override void SetHost(GameHost host) @@ -134,8 +143,26 @@ namespace osu.Desktop desktopWindow.DragDrop += f => fileDrop(new[] { f }); } + public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler) + { + switch (handler) + { + case ITabletHandler th: + return new TabletSettings(th); + + case MouseHandler mh: + return new MouseSettings(mh); + + case JoystickHandler jh: + return new JoystickSettings(jh); + + default: + return base.CreateSettingsSubsectionFor(handler); + } + } + private readonly List importableFiles = new List(); - private ScheduledDelegate importSchedule; + private ScheduledDelegate? importSchedule; private void fileDrop(string[] filePaths) { @@ -168,5 +195,11 @@ namespace osu.Desktop Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + osuSchemeLinkIPCChannel?.Dispose(); + } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 4ba9cc6a90..712f300671 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -11,6 +11,7 @@ using osu.Framework; using osu.Framework.Development; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game; using osu.Game.IPC; using osu.Game.Tournament; using Squirrel; @@ -65,19 +66,8 @@ namespace osu.Desktop { if (!host.IsPrimaryInstance) { - if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args - { - var importer = new ArchiveImportIPCChannel(host); - - foreach (string file in args) - { - Console.WriteLine(@"Importing {0}", file); - if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000)) - throw new TimeoutException(@"IPC took too long to send"); - } - + if (trySendIPCMessage(host, cwd, args)) return; - } // we want to allow multiple instances to be started when in debug. if (!DebugUtils.IsDebugBuild) @@ -108,21 +98,49 @@ namespace osu.Desktop } } + private static bool trySendIPCMessage(IIpcHost host, string cwd, string[] args) + { + if (args.Length == 1 && args[0].StartsWith(OsuGameBase.OSU_PROTOCOL, StringComparison.Ordinal)) + { + var osuSchemeLinkHandler = new OsuSchemeLinkIPCChannel(host); + if (!osuSchemeLinkHandler.HandleLinkAsync(args[0]).Wait(3000)) + throw new IPCTimeoutException(osuSchemeLinkHandler.GetType()); + + return true; + } + + if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args + { + var importer = new ArchiveImportIPCChannel(host); + + foreach (string file in args) + { + Console.WriteLine(@"Importing {0}", file); + if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000)) + throw new IPCTimeoutException(importer.GetType()); + } + + return true; + } + + return false; + } + [SupportedOSPlatform("windows")] private static void setupSquirrel() { - SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) => + SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) => { tools.CreateShortcutForThisExe(); tools.CreateUninstallerRegistryEntry(); - }, onAppUpdate: (version, tools) => + }, onAppUpdate: (_, tools) => { tools.CreateUninstallerRegistryEntry(); - }, onAppUninstall: (version, tools) => + }, onAppUninstall: (_, tools) => { tools.RemoveShortcutForThisExe(); tools.RemoveUninstallerRegistryEntry(); - }, onEveryRun: (version, tools, firstRun) => + }, onEveryRun: (_, _, _) => { // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently // causes the right-click context menu to function incorrectly. diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 59bc2dabb2..4e5f8d37b1 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -154,7 +154,7 @@ namespace osu.Desktop.Updater Activated = () => { updateManager.PrepareUpdateAsync() - .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit())); + .ContinueWith(_ => updateManager.Schedule(() => game?.AttemptExit())); return true; }; } diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs index bdc24315bf..5ffda6504e 100644 --- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs +++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs @@ -31,7 +31,7 @@ namespace osu.Game.Benchmarks realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME); - realm.Run(r => + realm.Run(_ => { realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo }))); }); @@ -76,7 +76,7 @@ namespace osu.Game.Benchmarks } }); - done.Wait(); + done.Wait(60000); } [Benchmark] @@ -115,7 +115,7 @@ namespace osu.Game.Benchmarks } }); - done.Wait(); + done.Wait(60000); } [Benchmark] diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 54d26a0f3d..0de992c1df 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Tests new JuiceStreamPathVertex(20, -5) })); - removeCount = path.RemoveVertices((_, i) => true); + removeCount = path.RemoveVertices((_, _) => true); Assert.That(removeCount, Is.EqualTo(1)); Assert.That(path.Vertices, Is.EqualTo(new[] { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 731cb4e135..8dd6f82c57 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("change component scale", () => Player.ChildrenOfType().First().Scale = new Vector2(2f)); AddStep("update target", () => Player.ChildrenOfType().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("exit player", () => Player.Exit()); - CreateTest(null); + CreateTest(); } AddAssert("legacy HUD combo counter hidden", () => diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 655edf7e08..4886942dc6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests hyperDashCount = 0; // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. - Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => + Player.DrawableRuleset.FrameStableComponents.OnUpdate += _ => { var catcher = Player.ChildrenOfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponent.cs b/osu.Game.Rulesets.Catch/CatchSkinComponent.cs index 0fcdd34ca3..e79da667da 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponent.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponent.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME; - protected override string ComponentName => Component.ToString().ToLower(); + protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 9951d736c3..2d01153f98 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -1,10 +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 System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Catch.Difficulty @@ -31,9 +30,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty yield return (ATTRIB_ID_MAX_COMBO, MaxCombo); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values) + public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values); + base.FromDatabaseAttributes(values, onlineInfo); StarRating = values[ATTRIB_ID_AIM]; ApproachRate = values[ATTRIB_ID_APPROACH_RATE]; diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 01156ab021..f31dc3ef9c 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -135,15 +135,15 @@ namespace osu.Game.Rulesets.Catch.Edit { switch (BlueprintContainer.CurrentTool) { - case SelectTool _: + case SelectTool: if (EditorBeatmap.SelectedHitObjects.Count == 0) return null; double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime); return getLastSnappableHitObject(minTime); - case FruitCompositionTool _: - case JuiceStreamCompositionTool _: + case FruitCompositionTool: + case JuiceStreamCompositionTool: if (!CursorInPlacementArea) return null; diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs index 4390234b59..889d3909bd 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectUtils.cs @@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Catch.Edit case Droplet droplet: return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX); - case JuiceStream _: + case JuiceStream: return GetPositionRange(hitObject.NestedHitObjects); - case BananaShower _: + case BananaShower: // A banana shower occupies the whole screen width. return new PositionRange(0, CatchPlayfield.WIDTH); diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs index 319cb1bfc9..5aac521d0b 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Edit { switch (hitObject) { - case BananaShower _: + case BananaShower: return false; case JuiceStream juiceStream: diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 6a3ec336d1..efc841dfac 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -389,13 +389,13 @@ namespace osu.Game.Rulesets.Catch.UI { switch (source) { - case Fruit _: + case Fruit: return caughtFruitPool.Get(); - case Banana _: + case Banana: return caughtBananaPool.Get(); - case Droplet _: + case Droplet: return caughtDropletPool.Get(); default: diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 2b4f497785..90cd7f57b5 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps switch (original) { - case IHasDistance _: + case IHasDistance: { var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); conversion = generator; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index ffef8a50ad..d259c2af8e 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -1,10 +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 System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Mania.Difficulty @@ -30,9 +29,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values) + public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values); + base.FromDatabaseAttributes(values, onlineInfo); MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 98a492450e..4723416c30 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -146,56 +146,56 @@ namespace osu.Game.Rulesets.Mania { switch (mod) { - case ManiaModKey1 _: + case ManiaModKey1: value |= LegacyMods.Key1; break; - case ManiaModKey2 _: + case ManiaModKey2: value |= LegacyMods.Key2; break; - case ManiaModKey3 _: + case ManiaModKey3: value |= LegacyMods.Key3; break; - case ManiaModKey4 _: + case ManiaModKey4: value |= LegacyMods.Key4; break; - case ManiaModKey5 _: + case ManiaModKey5: value |= LegacyMods.Key5; break; - case ManiaModKey6 _: + case ManiaModKey6: value |= LegacyMods.Key6; break; - case ManiaModKey7 _: + case ManiaModKey7: value |= LegacyMods.Key7; break; - case ManiaModKey8 _: + case ManiaModKey8: value |= LegacyMods.Key8; break; - case ManiaModKey9 _: + case ManiaModKey9: value |= LegacyMods.Key9; break; - case ManiaModDualStages _: + case ManiaModDualStages: value |= LegacyMods.KeyCoop; break; - case ManiaModFadeIn _: + case ManiaModFadeIn: value |= LegacyMods.FadeIn; value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that. break; - case ManiaModMirror _: + case ManiaModMirror: value |= LegacyMods.Mirror; break; - case ManiaModRandom _: + case ManiaModRandom: value |= LegacyMods.Random; break; } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index b709f85523..21b362df00 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; - protected override string ComponentName => Component.ToString().ToLower(); + protected override string ComponentName => Component.ToString().ToLowerInvariant(); } public enum ManiaSkinComponents diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index c7c7a6003e..22347d21b8 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods var rng = new Random((int)Seed.Value); int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; - var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList(); + var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(_ => rng.Next()).ToList(); beatmap.HitObjects.OfType().ForEach(h => h.Column = shuffledColumns[h.Column]); } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 3814ad84f1..26572de412 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -57,11 +57,11 @@ namespace osu.Game.Rulesets.Mania.Replays { switch (point) { - case HitPoint _: + case HitPoint: actions.Add(columnActions[point.Column]); break; - case ReleasePoint _: + case ReleasePoint: actions.Remove(columnActions[point.Column]); break; } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 3d9fe37e0f..ef9e332253 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -10,7 +10,6 @@ using osu.Framework.Input; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit; @@ -41,10 +40,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); - private bool editorComponentsReady => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && editor?.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; - [TestCase(true)] [TestCase(false)] public void TestVelocityChangeSavesCorrectly(bool adjustVelocity) @@ -52,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor double? velocity = null; AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editorComponentsReady); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); @@ -91,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit", () => InputManager.Key(Key.Escape)); AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editorComponentsReady); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); AddAssert("slider has correct velocity", () => slider.Velocity == velocity); diff --git a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs index 2c4310202e..7c4ab2f5f4 100644 --- a/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/LegacyMainCirclePieceTest.cs @@ -76,7 +76,7 @@ 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 _) => new Texture(1, 1) { AssetName = componentName }); Child = new DependencyProvidingContainer { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index fe8bba3ed8..4f6d6376bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods lastResult = null; spinner = nextSpinner; - spinner.OnNewResult += (o, result) => lastResult = result; + spinner.OnNewResult += (_, result) => lastResult = result; } return lastResult?.Type == HitResult.Great; @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods return false; spinner = nextSpinner; - spinner.OnNewResult += (o, result) => results.Add(result); + spinner.OnNewResult += (_, result) => results.Add(result); results.Clear(); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs new file mode 100644 index 0000000000..7a6e19575e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [HeadlessTest] + public class TestSceneSliderFollowCircleInput : RateAdjustedBeatmapTestScene + { + private List? judgementResults; + private ScoreAccessibleReplayPlayer? currentPlayer; + + [Test] + public void TestMaximumDistanceTrackingWithoutMovement( + [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] + float circleSize, + [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] + double velocity) + { + const double time_slider_start = 1000; + + float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2; + float followCircleRadius = circleRadius * 1.2f; + + performTest(new Beatmap + { + HitObjects = + { + new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity }, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(followCircleRadius, 0), + }, followCircleRadius), + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + CircleSize = circleSize, + SliderTickRate = 1 + }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }, new List + { + new OsuReplayFrame { Position = new Vector2(-circleRadius + 1, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start }, + }); + + AddAssert("Tracking kept", assertMaxJudge); + } + + private bool assertMaxJudge() => judgementResults?.Any() == true && judgementResults.All(t => t.Type == t.Judgement.MaxResult); + + private void performTest(Beatmap beatmap, List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults?.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer?.ScoreProcessor.HasCompleted.Value == true); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index be2e9c7cb5..366793058d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -66,10 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests drawableSlider = null; }); - [SetUpSteps] - public override void SetUpSteps() - { - } + protected override bool HasCustomSteps => true; [TestCase(0)] [TestCase(1)] @@ -77,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSnakingEnabled(int sliderIndex) { AddStep("enable autoplay", () => autoplay = true); - base.SetUpSteps(); + CreateTest(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); retrieveSlider(sliderIndex); @@ -101,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSnakingDisabled(int sliderIndex) { AddStep("have autoplay", () => autoplay = true); - base.SetUpSteps(); + CreateTest(); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); retrieveSlider(sliderIndex); @@ -121,8 +118,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable autoplay", () => autoplay = true); setSnaking(true); - base.SetUpSteps(); - + CreateTest(); // repeat might have a chance to update its position depending on where in the frame its hit, // so some leniency is allowed here instead of checking strict equality addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame); @@ -133,15 +129,14 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("disable autoplay", () => autoplay = false); setSnaking(true); - base.SetUpSteps(); - + CreateTest(); addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased); } private void retrieveSlider(int index) { AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]); - addSeekStep(() => slider); + addSeekStep(() => slider.StartTime); AddUntilStep("retrieve drawable slider", () => (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); } @@ -161,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Tests => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex; - private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)getSliderStart : getSliderEnd; + private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd; private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; private Vector2 getSliderStart() => getSliderCurve().First(); @@ -205,16 +200,10 @@ namespace osu.Game.Rulesets.Osu.Tests }); } - private void addSeekStep(Func slider) + private void addSeekStep(Func getTime) { - AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); - } - - private void addSeekStep(Func time) - { - AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time())); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddStep("seek to time", () => Player.GameplayClockContainer.Seek(getTime())); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(getTime(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() }; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 4b1e12d5c6..5187717639 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -4,7 +4,6 @@ #nullable disable using System; -using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -33,14 +32,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; + var osuNextObj = (OsuDifficultyHitObject)current.Next(0); double strainTime = osuCurrObj.StrainTime; double greatWindowFull = greatWindow * 2; - double speedWindowRatio = strainTime / greatWindowFull; + double doubletapness = 1; - // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) - if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime) - strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio); + // Nerf doubletappable doubles. + if (osuNextObj != null) + { + double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime); + double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime); + double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime); + double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference); + double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / greatWindowFull), 2); + doubletapness = Math.Pow(speedRatio, 1 - windowRatio); + } // Cap deltatime to the OD 300 hitwindow. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. @@ -55,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double travelDistance = osuPrevObj?.TravelDistance ?? 0; double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance); - return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; + return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 19217015c1..4b06502e32 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -1,12 +1,11 @@ // 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 JetBrains.Annotations; using Newtonsoft.Json; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -96,9 +95,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values) + public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values); + base.FromDatabaseAttributes(values, onlineInfo); AimDifficulty = values[ATTRIB_ID_AIM]; SpeedDifficulty = values[ATTRIB_ID_SPEED]; @@ -108,6 +107,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty StarRating = values[ATTRIB_ID_DIFFICULTY]; FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; + + DrainRate = onlineInfo.DrainRate; + HitCircleCount = onlineInfo.CircleCount; + SliderCount = onlineInfo.SliderCount; + SpinnerCount = onlineInfo.SpinnerCount; } #region Newtonsoft.Json implicit ShouldSerialize() methods diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 695bb2f8d0..fffa886dd0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -7,6 +7,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Evaluators; +using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { - currentStrain *= strainDecay(current.DeltaTime); + currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier; currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 066a114f66..60896b17bf 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit }); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); - selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); + selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject.ValueChanged += _ => updateDistanceSnapGrid(); @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit switch (BlueprintContainer.CurrentTool) { - case SelectTool _: + case SelectTool: if (!EditorBeatmap.SelectedHitObjects.Any()) return; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs index 3d4a26b3ff..e25845f5ab 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { - drawable.ApplyCustomUpdateState += (drawableObject, state) => + drawable.ApplyCustomUpdateState += (drawableObject, _) => { if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8a15d730cd..00009f4c3d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] public Bindable ClassicNoteLock { get; } = new BindableBool(true); - [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")] - public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true); - [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true); @@ -62,10 +59,6 @@ namespace osu.Game.Rulesets.Osu.Mods { switch (obj) { - case DrawableSlider slider: - slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value; - break; - case DrawableSliderHead head: head.TrackFollowCircle = !NoSliderHeadMovement.Value; break; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index f9422e1ff9..11ceb0f710 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawableObject) { - case DrawableSliderTail _: + case DrawableSliderTail: using (drawableObject.BeginAbsoluteSequence(fadeStartTime)) drawableObject.FadeOut(fadeDuration); @@ -165,14 +165,14 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { - case Slider _: + case Slider: return (fadeOutStartTime, longFadeDuration); - case SliderTick _: + case SliderTick: double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration); - case Spinner _: + case Spinner: return (fadeOutStartTime + longFadeDuration, fadeOutDuration); default: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index d96724929f..44942e9e37 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Mods // apply grow effect switch (drawable) { - case DrawableSliderHead _: - case DrawableSliderTail _: + case DrawableSliderHead: + case DrawableSliderTail: // special cases we should *not* be scaling. break; - case DrawableSlider _: - case DrawableHitCircle _: + case DrawableSlider: + case DrawableHitCircle: { using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 4589a8eca1..f03bcffdc8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -374,7 +374,7 @@ namespace osu.Game.Rulesets.Osu.Mods int i = 0; double currentTime = timingPoint.Time; - while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) + while (!definitelyBigger(currentTime, mapEndTime) && ReferenceEquals(controlPointInfo.TimingPointAt(currentTime), timingPoint)) { beats.Add(Math.Floor(currentTime)); i++; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 51994a3e1a..5a08df3803 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Osu.Mods { switch (drawable) { - case DrawableSliderHead _: - case DrawableSliderTail _: - case DrawableSliderTick _: - case DrawableSliderRepeat _: + case DrawableSliderHead: + case DrawableSliderTail: + case DrawableSliderTick: + case DrawableSliderRepeat: return; default: diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index dcb47347ef..a5468ff613 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects public Slider() { - SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples(); + SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples(); Path.Version.ValueChanged += _ => updateNestedPositions(); } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8b9dc71a8b..120ce32612 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -120,19 +120,19 @@ namespace osu.Game.Rulesets.Osu { switch (mod) { - case OsuModAutopilot _: + case OsuModAutopilot: value |= LegacyMods.Autopilot; break; - case OsuModSpunOut _: + case OsuModSpunOut: value |= LegacyMods.SpunOut; break; - case OsuModTarget _: + case OsuModTarget: value |= LegacyMods.Target; break; - case OsuModTouchDevice _: + case OsuModTouchDevice: value |= LegacyMods.TouchDevice; break; } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponent.cs b/osu.Game.Rulesets.Osu/OsuSkinComponent.cs index 82c4005c5e..0abaf2c924 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponent.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponent.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu protected override string RulesetPrefix => OsuRuleset.SHORT_NAME; - protected override string ComponentName => Component.ToString().ToLower(); + protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 27029afece..b0155c02cf 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Replays hitWindows = slider.TailCircle.HitWindows; break; - case Spinner _: + case Spinner: hitWindows = defaultHitWindows; break; } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 7a71ef6c65..34a1bd40e9 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Scoring { switch (hitObject) { - case HitCircle _: + case HitCircle: return new OsuHitCircleJudgementResult(hitObject, judgement); default: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index d2ea8f1660..389e9343e7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -34,13 +34,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default set => ball.Colour = value; } - /// - /// Whether to track accurately to the visual size of this . - /// If false, tracking will be performed at the final scale at all times. - /// - public bool InputTracksVisualSize = true; - private readonly Drawable followCircle; + private readonly Drawable fullSizeFollowCircle; private readonly DrawableSlider drawableSlider; private readonly Drawable ball; @@ -62,6 +57,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, + fullSizeFollowCircle = new CircularContainer + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true + }, ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) { Anchor = Anchor.Centre, @@ -104,14 +106,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default tracking = value; - if (InputTracksVisualSize) - followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); - else - { - // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. - followCircle.ScaleTo(tracking ? 2.4f : 1f); - } + fullSizeFollowCircle.Scale = new Vector2(tracking ? 2.4f : 1f); + followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); } } @@ -170,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default // in valid time range Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && // in valid position range - lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && + lastScreenSpaceMousePosition.HasValue && fullSizeFollowCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action (actions?.Any(isValidTrackingAction) ?? false); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index e4ca0d2ea8..d5cc469ca9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy break; - case DrawableSpinnerBonusTick _: + case DrawableSpinnerBonusTick: if (state == ArmedState.Hit) glow.FlashColour(Color4.White, 200); diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs index fba225d464..6330208d37 100644 --- a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI switch (obj) { - case DrawableSpinner _: + case DrawableSpinner: continue; case DrawableSlider slider: diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ad27428010..3179b37d5a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.UI // note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded. switch (drawable) { - case DrawableSpinner _: + case DrawableSpinner: spinnerProxies.Add(drawable.CreateProxy()); break; diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index c413226e63..3a156d4d25 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -88,11 +88,11 @@ namespace osu.Game.Rulesets.Osu.Utils switch (hitObject) { - case HitCircle _: + case HitCircle: shift = clampHitCircleToPlayfield(current); break; - case Slider _: + case Slider: shift = clampSliderToPlayfield(current); break; } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 1bf6c0560a..ef95358d34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); - AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear)); + AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear)); } private void setBeatmap(bool kiai = false) @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { TaikoMascotAnimationState[] mascotStates = null; - AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", + AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", () => { applyNewResult(judgementResult); @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray()); }); - AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState)); + AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState)); } private void applyNewResult(JudgementResult judgementResult) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 51772df4a7..cdfab4a215 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Setup judgements", () => { judged = false; - Player.ScoreProcessor.NewJudgement += b => judged = true; + Player.ScoreProcessor.NewJudgement += _ => judged = true; }); AddUntilStep("swell judged", () => judged); AddAssert("failed", () => Player.GameplayState.HasFailed); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 6c617a22a4..380ab4a4fc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -1,10 +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 System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -57,9 +56,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); } - public override void FromDatabaseAttributes(IReadOnlyDictionary values) + public override void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { - base.FromDatabaseAttributes(values); + base.FromDatabaseAttributes(values, onlineInfo); MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; StarRating = values[ATTRIB_ID_DIFFICULTY]; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 69eace4302..e065bb43fd 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Taiko.Mods { switch (hitObject) { - case DrawableDrumRollTick _: - case DrawableHit _: + case DrawableDrumRollTick: + case DrawableHit: double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier; double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time; double duration = preempt * fade_out_duration; diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 8bc0dc6df0..20f3304c30 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM; }); - SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); + SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples()); } private void updateTypeFromSamples() diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index b7bdd98d2a..d4d59d5d44 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Objects protected TaikoStrongableHitObject() { IsStrongBindable.BindValueChanged(_ => updateSamplesFromType()); - SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); + SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples()); } private void updateTypeFromSamples() diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs index 676a3d4bc3..63314a6822 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Taiko protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME; - protected override string ComponentName => Component.ToString().ToLower(); + protected override string ComponentName => Component.ToString().ToLowerInvariant(); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index bacd9b41f8..26a37fc464 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -139,10 +139,10 @@ namespace osu.Game.Rulesets.Taiko.UI private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) { - var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + var texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}{frameIndex}"); if (frameIndex == 0 && texture == null) - texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}"); + texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}"); return texture; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4e0c8029fb..4ef7c24464 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Taiko.UI barLinePlayfield.Add(barLine); break; - case DrawableTaikoHitObject _: + case DrawableTaikoHitObject: base.Add(h); break; @@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Taiko.UI case DrawableBarLine barLine: return barLinePlayfield.Remove(barLine); - case DrawableTaikoHitObject _: + case DrawableTaikoHitObject: return base.Remove(h); default: @@ -280,12 +280,12 @@ namespace osu.Game.Rulesets.Taiko.UI switch (result.Judgement) { - case TaikoStrongJudgement _: + case TaikoStrongJudgement: if (result.IsHit) hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result); break; - case TaikoDrumRollTickJudgement _: + case TaikoDrumRollTickJudgement: if (!result.IsHit) break; diff --git a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs index f87e5711a6..6eb103316f 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneBeatmapDifficultyCache.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Beatmaps [TestCase(8.3, DifficultyRating.ExpertPlus)] public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) { - var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating); + var actualBracket = StarDifficulty.GetDifficultyRating(starRating); Assert.AreEqual(expectedBracket, actualBracket); } diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 7f466925a4..c31aafa67f 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Collections.IO public async Task TestImportMalformedDatabase() { bool exceptionThrown = false; - UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true; + UnhandledExceptionEventHandler setException = (_, _) => exceptionThrown = true; using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index f9cd75d8c3..6f5a79c9da 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -15,7 +15,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; @@ -39,10 +38,7 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapImporter(storage, realm)) using (new RealmRulesetStore(realm, storage)) { - Live? beatmapSet; - - using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) - beatmapSet = await importer.Import(reader); + var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz")); Assert.NotNull(beatmapSet); Debug.Assert(beatmapSet != null); @@ -83,10 +79,7 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapImporter(storage, realm)) using (new RealmRulesetStore(realm, storage)) { - Live? beatmapSet; - - using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) - beatmapSet = await importer.Import(reader); + var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz")); Assert.NotNull(beatmapSet); Debug.Assert(beatmapSet != null); @@ -146,11 +139,8 @@ namespace osu.Game.Tests.Database { Task.Run(async () => { - Live? beatmapSet; - - using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) - // ReSharper disable once AccessToDisposedClosure - beatmapSet = await importer.Import(reader); + // ReSharper disable once AccessToDisposedClosure + var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz")); Assert.NotNull(beatmapSet); Debug.Assert(beatmapSet != null); @@ -173,13 +163,8 @@ namespace osu.Game.Tests.Database using (var importer = new BeatmapImporter(storage, realm)) using (new RealmRulesetStore(realm, storage)) { - Live? imported; - - using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) - { - imported = await importer.Import(reader); - EnsureLoaded(realm.Realm); - } + var imported = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz")); + EnsureLoaded(realm.Realm); Assert.AreEqual(1, realm.Realm.All().Count()); @@ -291,6 +276,42 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestImportDirectoryWithEmptyOsuFiles() + { + RunTestWithRealmAsync(async (realm, storage) => + { + using var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + string? temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + foreach (var file in new DirectoryInfo(extractedFolder).GetFiles("*.osu")) + { + using (file.Open(FileMode.Create)) + { + // empty file. + } + } + + var imported = await importer.Import(new ImportTask(extractedFolder)); + Assert.IsNull(imported); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + [Test] public void TestImportThenImportWithReZip() { @@ -586,6 +607,12 @@ namespace osu.Game.Tests.Database using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew)) using (var zip = ZipArchive.Open(brokenOsz)) { + foreach (var entry in zip.Entries.ToArray()) + { + if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture)) + zip.RemoveEntry(entry); + } + zip.AddEntry("broken.osu", brokenOsu, false); zip.SaveTo(outStream, CompressionType.Deflate); } @@ -606,7 +633,7 @@ namespace osu.Game.Tests.Database checkSingleReferencedFileCount(realm.Realm, 18); - Assert.AreEqual(1, loggedExceptionCount); + Assert.AreEqual(0, loggedExceptionCount); File.Delete(brokenTempFilename); }); @@ -760,7 +787,7 @@ namespace osu.Game.Tests.Database } }; - var imported = importer.Import(toImport); + var imported = importer.ImportModel(toImport); realm.Run(r => r.Refresh()); diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index d10ab2ad2b..65f805bafb 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Database realm.RegisterCustomSubscription(r => { - var subscription = r.All().QueryAsyncWithNotifications((sender, changes, error) => + var subscription = r.All().QueryAsyncWithNotifications((_, _, _) => { realm.Run(_ => { @@ -79,11 +79,11 @@ namespace osu.Game.Tests.Database { hasThreadedUsage.Set(); - stopThreadedUsage.Wait(); + stopThreadedUsage.Wait(60000); }); }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); - hasThreadedUsage.Wait(); + hasThreadedUsage.Wait(60000); Assert.Throws(() => { diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 416216062e..00a667521d 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Database }); // Can't be used, even from within a valid context. - realm.Run(threadContext => + realm.Run(_ => { Assert.Throws(() => { diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index f9c4985219..c4e8bbfccf 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Gameplay AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddAssert("not failed", () => !processor.HasFailed); - AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); + AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddAssert("failed", () => processor.HasFailed); } diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7d5f0bcd0c..a803974d30 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -254,7 +254,7 @@ namespace osu.Game.Tests.NonVisual Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME))); Directory.CreateDirectory(customPath2); - File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)); + File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text"); // Fails because file already exists. Assert.Throws(() => osu.Migrate(customPath2)); diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs index a779fae510..14da07bc2d 100644 --- a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs +++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs @@ -83,14 +83,14 @@ namespace osu.Game.Tests.NonVisual public override event Action NewResult { - add => throw new InvalidOperationException(); - remove => throw new InvalidOperationException(); + add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); } public override event Action RevertResult { - add => throw new InvalidOperationException(); - remove => throw new InvalidOperationException(); + add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } public override Playfield Playfield { get; } diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 4a5bb6de46..541ad1e8bb 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -364,12 +364,12 @@ namespace osu.Game.Tests.NonVisual private void confirmCurrentFrame(int? frame) { - Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame"); + Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.CurrentFrame?.Time, "Unexpected current frame"); } private void confirmNextFrame(int? frame) { - Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame"); + Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.NextFrame?.Time, "Unexpected next frame"); } private class TestReplayFrame : ReplayFrame diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index efab884d37..22aa78838a 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.NonVisual.Skinning { // use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures. int width = 1; - Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image(width, width++))); + Textures = fileNames.ToDictionary(fileName => fileName, _ => new TextureUpload(new Image(width, width++))); } public TextureUpload Get(string name) => Textures.GetValueOrDefault(name); diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index e502a71f34..1d33a895eb 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -227,10 +227,10 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override Live Import(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) + public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) { testBeatmapManager.AllowImport.Task.WaitSafely(); - return (testBeatmapManager.CurrentImport = base.Import(item, archive, batchImport, cancellationToken)); + return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken)); } } } diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 4db88cbe82..8b7fcae1a9 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -15,7 +15,6 @@ using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; -using osu.Game.IO.Archives; using osu.Game.Skinning; using SharpCompress.Archives.Zip; @@ -28,7 +27,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSingleImportDifferentFilename() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSingleImportWeirdIniFileCase() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", iniFilename: "Skin.InI"), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); @@ -46,7 +45,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSingleImportMissingSectionHeader() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner", includeSectionHeader: false), "skin.osk")); // When the import filename doesn't match, it should be appended (and update the skin.ini). assertCorrectMetadata(import1, "test skin [skin]", "skinner", osu); @@ -55,7 +54,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSingleImportMatchingFilename() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "test skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "test skin.osk")); // When the import filename matches it shouldn't be appended. assertCorrectMetadata(import1, "test skin", "skinner", osu); @@ -64,7 +63,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSingleImportNoIniFile() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithNonIniFile(), "test skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithNonIniFile(), "test skin.osk")); // When the import filename matches it shouldn't be appended. assertCorrectMetadata(import1, "test skin", "Unknown", osu); @@ -73,7 +72,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestEmptyImportImportsWithFilename() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createEmptyOsk(), "test skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createEmptyOsk(), "test skin.osk")); // When the import filename matches it shouldn't be appended. assertCorrectMetadata(import1, "test skin", "Unknown", osu); @@ -86,8 +85,8 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk")); assertImportedOnce(import1, import2); }); @@ -96,8 +95,8 @@ namespace osu.Game.Tests.Skins.IO public Task TestImportTwiceWithNoMetadataSameDownloadFilename() => runSkinTest(async osu => { // if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety. - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk")); assertImportedOnce(import1, import2); }); @@ -105,10 +104,10 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestImportUpperCasedOskArchive() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.OsK")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.OsK")); assertCorrectMetadata(import1, "name 1", "author 1", osu); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "name 1.oSK")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "name 1.oSK")); assertImportedOnce(import1, import2); }); @@ -118,7 +117,7 @@ namespace osu.Game.Tests.Skins.IO { MemoryStream exportStream = new MemoryStream(); - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "custom.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "custom.osk")); assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu); import1.PerformRead(s => @@ -128,7 +127,7 @@ namespace osu.Game.Tests.Skins.IO string exportFilename = import1.GetDisplayString(); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(exportStream, $"{exportFilename}.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk")); assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu); assertImportedOnce(import1, import2); @@ -137,8 +136,8 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1")); assertImportedOnce(import1, import2); assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu); @@ -151,8 +150,8 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestImportTwiceWithSameMetadataButDifferentFilename() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin.osk")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin", "skinner"), "skin2.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin2.osk")); assertImportedBoth(import1, import2); }); @@ -161,8 +160,8 @@ namespace osu.Game.Tests.Skins.IO public Task TestImportTwiceWithNoMetadataDifferentDownloadFilename() => runSkinTest(async osu => { // if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety. - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download.osk")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni(string.Empty, string.Empty), "download2.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download2.osk")); assertImportedBoth(import1, import2); }); @@ -170,8 +169,8 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestImportTwiceWithSameFilenameDifferentMetadata() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2", "skinner"), "skin.osk")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("test skin v2.1", "skinner"), "skin.osk")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2", "skinner"), "skin.osk")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin v2.1", "skinner"), "skin.osk")); assertImportedBoth(import1, import2); assertCorrectMetadata(import1, "test skin v2 [skin]", "skinner", osu); @@ -181,8 +180,8 @@ namespace osu.Game.Tests.Skins.IO [Test] public Task TestSameMetadataNameDifferentFolderName() => runSkinTest(async osu => { - var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 1")); - var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "my custom skin 2")); + var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1")); + var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 2")); assertImportedBoth(import1, import2); assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu); @@ -358,10 +357,10 @@ namespace osu.Game.Tests.Skins.IO } } - private async Task> loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) + private async Task> loadSkinIntoOsu(OsuGameBase osu, ImportTask import) { var skinManager = osu.Dependencies.Get(); - return await skinManager.Import(archive); + return await skinManager.Import(import); } } } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs index 77ceef6402..1493c10969 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinLookupDisables.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Skins { AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups)); - ISkin expected() => allowBeatmapLookups ? (ISkin)beatmapSource : userSource; + ISkin expected() => allowBeatmapLookups ? beatmapSource : userSource; AddAssert("Check lookup is from correct source", () => requester.FindProvider(s => s.GetDrawableComponent(new TestSkinComponent()) != null) == expected()); } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index bd7e1f8ec5..f4cea2c8cc 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -10,7 +10,7 @@ using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.IO.Archives; +using osu.Game.Database; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Skins [BackgroundDependencyLoader] private void load() { - var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).GetResultSafely(); + var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-beatmap.osz"), "ogg-beatmap.osz")).GetResultSafely(); imported?.PerformRead(s => { diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 97588f4053..42c1eeb6d1 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -8,7 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Audio; -using osu.Game.IO.Archives; +using osu.Game.Database; using osu.Game.Skinning; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Skins [BackgroundDependencyLoader] private void load() { - var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).GetResultSafely(); + var imported = skins.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-skin.osk"), "ogg-skin.osk")).GetResultSafely(); skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo)); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 92f07af7b1..b711d55e15 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; - protected override bool IsolateSavingFromDatabase => false; [Resolved] @@ -95,15 +93,23 @@ namespace osu.Game.Tests.Visual.Editing string extractedFolder = $"{temp}_extracted"; Directory.CreateDirectory(extractedFolder); - using (var zip = ZipArchive.Open(temp)) - zip.WriteToDirectory(extractedFolder); + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); - bool success = setup.ChildrenOfType().First().ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); + bool success = setup.ChildrenOfType().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"))); - File.Delete(temp); - Directory.Delete(extractedFolder, true); + // ensure audio file is copied to beatmap as "audio.mp3" rather than original filename. + Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3"); - return success; + return success; + } + finally + { + File.Delete(temp); + Directory.Delete(extractedFolder, true); + } }); AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs new file mode 100644 index 0000000000..a21ef6f897 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs @@ -0,0 +1,60 @@ +// 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 NUnit.Framework; +using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Components.Timelines.Summary; +using osu.Game.Screens.Edit.GameplayTest; +using osu.Game.Screens.Select; +using osu.Game.Tests.Resources; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorNavigation : OsuGameTestScene + { + [Test] + public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + AddUntilStep("wait for completion notification", () => Game.Notifications.ChildrenOfType().Count() == 1); + AddStep("dismiss notifications", () => Game.Notifications.Hide()); + AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + AddStep("test gameplay", () => + { + var testGameplayButton = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(testGameplayButton); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded); + AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + + AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddAssert("previous ruleset restored", () => Game.Ruleset.Value.Equals(new ManiaRuleset().RulesetInfo)); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index cf1246ef07..e9bbf1e33d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -117,8 +117,8 @@ namespace osu.Game.Tests.Visual.Editing // After placement these must be non-default as defaults are read-only. AddAssert("Placed object has non-default control points", () => - EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && - EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); + !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) && + !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT)); ReloadEditorToSameBeatmap(); @@ -126,8 +126,8 @@ namespace osu.Game.Tests.Visual.Editing // After placement these must be non-default as defaults are read-only. AddAssert("Placed object still has non-default control points", () => - EditorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT && - EditorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT); + !ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) && + !ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT)); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index f9a3695d65..a79ba0ae5d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -322,8 +322,8 @@ namespace osu.Game.Tests.Visual.Gameplay { switch (h) { - case TestPooledHitObject _: - case TestPooledParentHitObject _: + case TestPooledHitObject: + case TestPooledParentHitObject: return null; case TestParentHitObject p: diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 0b737f5110..ce01bf2fb5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void LoadComplete() { base.LoadComplete(); - HealthProcessor.FailConditions += (_, __) => true; + HealthProcessor.FailConditions += (_, _) => true; } private double lastFrequency = double.MaxValue; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 90a4b536bb..5e87eff717 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void LoadComplete() { base.LoadComplete(); - HealthProcessor.FailConditions += (_, __) => true; + HealthProcessor.FailConditions += (_, _) => true; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 70d7f6a28b..707f807e64 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -273,14 +273,14 @@ namespace osu.Game.Tests.Visual.Gameplay public override event Action NewResult { - add => throw new InvalidOperationException(); - remove => throw new InvalidOperationException(); + add => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(NewResult)} operations not supported in test context"); } public override event Action RevertResult { - add => throw new InvalidOperationException(); - remove => throw new InvalidOperationException(); + add => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); + remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); } public override Playfield Playfield { get; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 3bebf2b68b..f319290441 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestToggleEditor() { - AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility()); + AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility()); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 757a261de6..16593effd6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Gameplay Child = new SkinProvidingContainer(secondarySource) { RelativeSizeAxes = Axes.Both, - Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")) + Child = consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation")) } }; }); @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")))); + AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation")))); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); } @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation")))); + AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation")))); AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); AddStep("disable", () => target.Disable()); AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 1e517efef2..5fad661e9b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -167,11 +167,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("start failing sends", () => { spectatorClient.ShouldFailSendingFrames = true; - framesReceivedSoFar = replay.Frames.Count; frameSendAttemptsSoFar = spectatorClient.FrameSendAttempts; }); - AddUntilStep("wait for send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 5); + AddUntilStep("wait for next send attempt", () => + { + framesReceivedSoFar = replay.Frames.Count; + return spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 1; + }); + + AddUntilStep("wait for more send attempts", () => spectatorClient.FrameSendAttempts > frameSendAttemptsSoFar + 10); AddAssert("frames did not increase", () => framesReceivedSoFar == replay.Frames.Count); AddStep("stop failing sends", () => spectatorClient.ShouldFailSendingFrames = false); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 079d459beb..f0e184d727 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void createPlayerTest() { - CreateTest(null); + CreateTest(); AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); waitUntilStoryboardSamplesPlay(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 68d024e63f..e2b2ad85a3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("set dim level to 0", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0)); - AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false); + AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false); AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000); AddStep("set ShowResults = true", () => showResults = true); } @@ -52,17 +52,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStoryboardSkipOutro() { - CreateTest(null); + AddStep("set storyboard duration to long", () => currentStoryboardDuration = 200000); + CreateTest(); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); - AddAssert("player is no longer current screen", () => !Player.IsCurrentScreen()); + AddUntilStep("player is no longer current screen", () => !Player.IsCurrentScreen()); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } [Test] public void TestStoryboardNoSkipOutro() { - CreateTest(null); + CreateTest(); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for score shown", () => Player.IsScoreShown); } @@ -70,7 +71,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStoryboardExitDuringOutroStillExits() { - CreateTest(null); + CreateTest(); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); AddAssert("player exited", () => !Player.IsCurrentScreen() && Player.GetChildScreen() == null); @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(true)] public void TestStoryboardToggle(bool enabledAtBeginning) { - CreateTest(null); + CreateTest(); AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning)); AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning)); AddUntilStep("wait for score shown", () => Player.IsScoreShown); @@ -91,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay { CreateTest(() => { - AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); + AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true); // Fail occurs at 164ms with the provided beatmap. // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience. @@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Gameplay { SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType().First(); - CreateTest(null); + CreateTest(); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible); @@ -143,7 +144,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPerformExitNoOutro() { - CreateTest(null); + CreateTest(); AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("exit via pause", () => Player.ExitViaPause()); diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs index 43ca47778a..631f2e707a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer // To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*. // This tracks the list of users 1:1. - MultiplayerUsers.BindCollectionChanged((c, e) => + MultiplayerUsers.BindCollectionChanged((_, e) => { switch (e.Action) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs index fc3079cba0..be1f21a7b2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs @@ -42,11 +42,11 @@ namespace osu.Game.Tests.Visual.Multiplayer var mockLounge = new Mock(); mockLounge .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>())) - .Callback, Action>((a, b, c, d) => + .Callback, Action>((_, _, _, d) => { Task.Run(() => { - allowResponseCallback.Wait(); + allowResponseCallback.Wait(10000); allowResponseCallback.Reset(); Schedule(() => d?.Invoke("Incorrect password")); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs index 0cdc144b6a..a800b21bc9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Multiplayer setRoomCountdown(countdownStart.Duration); break; - case StopCountdownRequest _: + case StopCountdownRequest: multiplayerRoom.Countdown = null; raiseRoomUpdated(); break; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs new file mode 100644 index 0000000000..1c2b1fe37d --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneInterProcessCommunication.cs @@ -0,0 +1,86 @@ +// 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 NUnit.Framework; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.IPC; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Navigation +{ + [TestFixture] + [Ignore("This test cannot be run headless, as it requires the game host running the nested game to have IPC bound.")] + public class TestSceneInterProcessCommunication : OsuGameTestScene + { + private HeadlessGameHost ipcSenderHost = null!; + + private OsuSchemeLinkIPCChannel osuSchemeLinkIPCReceiver = null!; + private OsuSchemeLinkIPCChannel osuSchemeLinkIPCSender = null!; + + private const int requested_beatmap_set_id = 1; + + [Resolved] + private GameHost gameHost { get; set; } = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("set up request handling", () => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest gbr: + + var apiBeatmapSet = CreateAPIBeatmapSet(); + apiBeatmapSet.OnlineID = requested_beatmap_set_id; + apiBeatmapSet.Beatmaps = apiBeatmapSet.Beatmaps.Append(new APIBeatmap + { + DifficultyName = "Target difficulty", + OnlineID = 75, + }).ToArray(); + gbr.TriggerSuccess(apiBeatmapSet); + return true; + } + + return false; + }; + }); + AddStep("create IPC receiver channel", () => osuSchemeLinkIPCReceiver = new OsuSchemeLinkIPCChannel(gameHost, Game)); + AddStep("create IPC sender channel", () => + { + ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true }); + osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); + }); + } + + [Test] + public void TestOsuSchemeLinkIPCChannel() + { + AddStep("open beatmap via IPC", () => osuSchemeLinkIPCSender.HandleLinkAsync($@"osu://s/{requested_beatmap_set_id}").WaitSafely()); + AddUntilStep("beatmap overlay displayed", () => Game.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddUntilStep("beatmap overlay showing content", () => Game.ChildrenOfType().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id); + } + + public override void TearDownSteps() + { + AddStep("dispose IPC receiver", () => osuSchemeLinkIPCReceiver.Dispose()); + AddStep("dispose IPC sender", () => + { + osuSchemeLinkIPCSender.Dispose(); + ipcSenderHost.Dispose(); + }); + base.TearDownSteps(); + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 78ab7947a7..2f0f2f68a5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -8,7 +8,9 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -16,6 +18,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning.Editor; using osu.Game.Tests.Beatmaps.IO; +using osuTK; using osuTK.Input; using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation; @@ -26,29 +29,6 @@ namespace osu.Game.Tests.Visual.Navigation private TestPlaySongSelect songSelect; private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); - private void advanceToSongSelect() - { - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); - - AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); - - AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - } - - private void openSkinEditor() - { - AddStep("open skin editor", () => - { - InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(Key.ShiftLeft); - InputManager.Key(Key.S); - InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(Key.ShiftLeft); - }); - AddUntilStep("skin editor loaded", () => skinEditor != null); - } - [Test] public void TestEditComponentDuringGameplay() { @@ -88,6 +68,68 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestComponentsDeselectedOnSkinEditorHide() + { + advanceToSongSelect(); + openSkinEditor(); + switchToGameplayScene(); + + AddUntilStep("wait for components", () => skinEditor.ChildrenOfType().Any()); + + AddStep("select all components", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.A); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("components selected", () => skinEditor.SelectedComponents.Count > 0); + + toggleSkinEditor(); + + AddUntilStep("no components selected", () => skinEditor.SelectedComponents.Count == 0); + } + + [Test] + public void TestSwitchScreenWhileDraggingComponent() + { + Vector2 firstBlueprintCentre = Vector2.Zero; + ScheduledDelegate movementDelegate = null; + + advanceToSongSelect(); + + openSkinEditor(); + + AddStep("add skinnable component", () => + { + skinEditor.ChildrenOfType().First().TriggerClick(); + }); + + AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents.Count == 1); + + AddStep("start drag", () => + { + firstBlueprintCentre = skinEditor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; + + InputManager.MoveMouseTo(firstBlueprintCentre); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("start movement", () => movementDelegate = Scheduler.AddDelayed(() => { InputManager.MoveMouseTo(firstBlueprintCentre += new Vector2(1)); }, 10, true)); + + toggleSkinEditor(); + AddStep("exit song select", () => songSelect.Exit()); + + AddUntilStep("wait for blueprints removed", () => !skinEditor.ChildrenOfType().Any()); + + AddStep("stop drag", () => + { + InputManager.ReleaseButton(MouseButton.Left); + movementDelegate?.Cancel(); + }); + } + [Test] public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay() { @@ -146,9 +188,40 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); } + private void advanceToSongSelect() + { + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + } + + private void openSkinEditor() + { + toggleSkinEditor(); + AddUntilStep("skin editor loaded", () => skinEditor != null); + } + + private void toggleSkinEditor() + { + AddStep("toggle skin editor", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.S); + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + } + private void switchToGameplayScene() { - AddStep("Click gameplay scene button", () => skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("Click gameplay scene button", () => + { + InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay")); + InputManager.Click(MouseButton.Left); + }); AddUntilStep("wait for player", () => { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 1fb0195368..d2d9b9a9e5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -40,7 +40,6 @@ namespace osu.Game.Tests.Visual.Online private ChannelManager channelManager; private APIUser testUser; - private Channel testPMChannel; private Channel[] testChannels; private Channel testChannel1 => testChannels[0]; @@ -53,7 +52,6 @@ namespace osu.Game.Tests.Visual.Online public void SetUp() => Schedule(() => { testUser = new APIUser { Username = "test user", Id = 5071479 }; - testPMChannel = new Channel(testUser); testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); Child = new DependencyProvidingContainer @@ -80,6 +78,14 @@ namespace osu.Game.Tests.Visual.Online { switch (req) { + case CreateChannelRequest createRequest: + createRequest.TriggerSuccess(new APIChatChannel + { + ChannelID = ((int)createRequest.Channel.Id), + RecentMessages = new List() + }); + return true; + case GetUpdatesRequest getUpdates: getUpdates.TriggerFailure(new WebException()); return true; @@ -181,7 +187,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Listing is visible", () => listingIsVisible); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); waitForChannel1Visible(); } @@ -203,12 +209,11 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChannelCloseButton() { + var testPMChannel = new Channel(testUser); + AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join PM and public channels", () => - { - channelManager.JoinChannel(testChannel1); - channelManager.JoinChannel(testPMChannel); - }); + joinTestChannel(0); + joinChannel(testPMChannel); AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); AddStep("Click close button", () => { @@ -229,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online public void TestChatCommand() { AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); AddAssert("PM channel is selected", () => @@ -248,14 +253,16 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestMultiplayerChannelIsNotShown() { - Channel multiplayerChannel = null; + Channel multiplayerChannel; AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + + joinChannel(multiplayerChannel = new Channel(new APIUser()) { Name = "#mp_1", Type = ChannelType.Multiplayer, - })); + }); + AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType() .Where(item => item.IsPresent) @@ -269,7 +276,7 @@ namespace osu.Game.Tests.Visual.Online Message message = null; AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 1", () => { @@ -291,8 +298,8 @@ namespace osu.Game.Tests.Visual.Online Message message = null; AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + joinTestChannel(0); + joinTestChannel(1); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 2", () => { @@ -314,8 +321,8 @@ namespace osu.Game.Tests.Visual.Online Message message = null; AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + joinTestChannel(0); + joinTestChannel(1); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 2", () => { @@ -337,7 +344,7 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Send message in channel 1", () => { testChannel1.AddNewMessages(message = new Message @@ -357,7 +364,7 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Send message in channel 1", () => { testChannel1.AddNewMessages(message = new Message @@ -378,7 +385,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); waitForChannel1Visible(); AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); @@ -404,11 +411,11 @@ namespace osu.Game.Tests.Visual.Online chatOverlay.Show(); chatOverlay.SlowLoading = true; }); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + joinTestChannel(0); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddUntilStep("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading); - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + joinTestChannel(1); AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2))); AddUntilStep("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading); @@ -461,19 +468,17 @@ namespace osu.Game.Tests.Visual.Online Channel pmChannel1 = createPrivateChannel(); Channel pmChannel2 = createPrivateChannel(); - AddStep("Show overlay with channels", () => - { - channelManager.JoinChannel(testChannel1); - channelManager.JoinChannel(testChannel2); - channelManager.JoinChannel(pmChannel1); - channelManager.JoinChannel(pmChannel2); - channelManager.JoinChannel(announceChannel); - chatOverlay.Show(); - }); + joinTestChannel(0); + joinTestChannel(1); + joinChannel(pmChannel1); + joinChannel(pmChannel2); + joinChannel(announceChannel); + + AddStep("Show overlay", () => chatOverlay.Show()); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - waitForChannel1Visible(); + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); waitForChannel2Visible(); @@ -490,6 +495,18 @@ namespace osu.Game.Tests.Visual.Online waitForChannel1Visible(); } + private void joinTestChannel(int i) + { + AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i])); + AddUntilStep("wait for join completed", () => testChannels[i].Joined.Value); + } + + private void joinChannel(Channel channel) + { + AddStep($"Join channel {channel}", () => channelManager.JoinChannel(channel)); + AddUntilStep("wait for join completed", () => channel.Joined.Value); + } + private void waitForChannel1Visible() => AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel1); @@ -549,7 +566,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createPrivateChannel() { - int id = RNG.Next(0, 10000); + int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1); return new Channel(new APIUser { Id = id, @@ -559,7 +576,7 @@ namespace osu.Game.Tests.Visual.Online private Channel createAnnounceChannel() { - int id = RNG.Next(0, 10000); + int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1); return new Channel { Name = $"Announce {id}", diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs index f28eaf5ad0..266e98db15 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs @@ -219,7 +219,7 @@ namespace osu.Game.Tests.Visual.Online { base.LoadComplete(); - Metadata.BindValueChanged(metadata => + Metadata.BindValueChanged(_ => { foreach (var b in this.ChildrenOfType()) b.Action = () => YearChanged?.Invoke(b.Year); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index a46e675370..8a04cd96fe 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -187,8 +187,8 @@ namespace osu.Game.Tests.Visual.Playlists // pre-check for requests we should be handling (as they are scheduled below). switch (request) { - case ShowPlaylistUserScoreRequest _: - case IndexPlaylistScoresRequest _: + case ShowPlaylistUserScoreRequest: + case IndexPlaylistScoresRequest: break; default: diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index abf65602f9..9791bb6248 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Settings public void ToggleVisibility() { AddWaitStep("wait some", 5); - AddToggleStep("toggle visibility", visible => settings.ToggleVisibility()); + AddToggleStep("toggle visibility", _ => settings.ToggleVisibility()); } [Test] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index 77670c38f3..4510fda11d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { - Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException(), + Ruleset = rulesets.GetRuleset(3) ?? throw new InvalidOperationException("osu!mania ruleset not found"), Difficulty = new BeatmapDifficulty { CircleSize = 5, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 6490fd822e..f9d18f4236 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -91,19 +91,19 @@ namespace osu.Game.Tests.Visual.SongSelect switch (instance) { - case OsuRuleset _: + case OsuRuleset: testInfoLabels(5); break; - case TaikoRuleset _: + case TaikoRuleset: testInfoLabels(5); break; - case CatchRuleset _: + case CatchRuleset: testInfoLabels(5); break; - case ManiaRuleset _: + case ManiaRuleset: testInfoLabels(4); break; diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 9fcd470d17..6d881555da 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -872,10 +872,10 @@ namespace osu.Game.Tests.Visual.SongSelect return set != null; }); - FilterableGroupedDifficultyIcon groupIcon = null; + GroupedDifficultyIcon groupIcon = null; AddUntilStep("Find group icon for different ruleset", () => { - return (groupIcon = set.ChildrenOfType() + return (groupIcon = set.ChildrenOfType() .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3)) != null; }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 27c107a2db..368babc9b5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Set time before zero", () => { - beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + beatContainer.NewBeat = (i, timingControlPoint, _, _) => { lastActuationTime = gameplayClockContainer.CurrentTime; lastTimingPoint = timingControlPoint; @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Set time before zero", () => { - beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + beatContainer.NewBeat = (i, timingControlPoint, _, _) => { lastBeatIndex = i; lastBpm = timingControlPoint.BPM; @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("bind event", () => { - beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => lastBpm = timingControlPoint.BPM; + beatContainer.NewBeat = (_, timingControlPoint, _, _) => lastBpm = timingControlPoint.BPM; }); AddUntilStep("wait for trigger", () => lastBpm != null); @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.UserInterface actualEffectPoint = null; beatContainer.AllowMistimedEventFiring = false; - beatContainer.NewBeat = (i, timingControlPoint, effectControlPoint, channelAmplitudes) => + beatContainer.NewBeat = (_, _, effectControlPoint, _) => { if (Precision.AlmostEquals(gameplayClockContainer.CurrentTime + earlyActivationMilliseconds, expectedEffectPoint.Time, BeatSyncedContainer.MISTIMED_ALLOWANCE)) actualEffectPoint = effectControlPoint; @@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.UserInterface private TimingControlPoint getNextTimingPoint(TimingControlPoint current) { - if (timingPoints[^1] == current) + if (ReferenceEquals(timingPoints[^1], current)) return current; int index = timingPoints.IndexOf(current); // -1 means that this is a "default beat" @@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.UserInterface { if (timingPoints.Count == 0) return 0; - if (timingPoints[^1] == current) + if (ReferenceEquals(timingPoints[^1], current)) { Debug.Assert(BeatSyncSource.Clock != null); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 1107fad834..44f2da2b95 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -77,13 +77,13 @@ namespace osu.Game.Tests.Visual.UserInterface }; control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); - control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true); + control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); - control.Extra.BindCollectionChanged((u, v) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); - control.Ranks.BindCollectionChanged((u, v) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true); + control.Extra.BindCollectionChanged((_, _) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); + control.Ranks.BindCollectionChanged((_, _) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); control.ExplicitContent.BindValueChanged(e => explicitMap.Text = $"Explicit Maps: {e.NewValue}", true); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index a3ae55670a..b845b85e1f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.Reset(); performer.Setup(g => g.PerformFromScreen(It.IsAny>(), It.IsAny>())) - .Callback((Action action, IEnumerable types) => action(null)); + .Callback((Action action, IEnumerable _) => action(null)); notificationOverlay.Setup(n => n.Post(It.IsAny())) .Callback((Notification n) => lastNotification = n); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs index f717bb4dee..7ce0fceff9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledDrawable.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre, Width = 500, AutoSizeAxes = Axes.Y, - Child = component = padded ? (LabelledDrawable)new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(), + Child = component = padded ? new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(), }; component.Label = "a sample component"; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index 50817bf804..72cddc0ad2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -9,9 +9,11 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Mods.Input; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Utils; @@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + [TestCase(ModType.DifficultyReduction)] [TestCase(ModType.DifficultyIncrease)] [TestCase(ModType.Conversion)] @@ -132,14 +137,16 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestKeyboardSelection() + public void TestSequentialKeyboardSelection() { + AddStep("set sequential hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential)); + ModColumn column = null!; AddStep("create content", () => Child = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(30), - Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }) + Child = column = new ModColumn(ModType.DifficultyReduction, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -158,9 +165,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set filter to NF", () => setFilter(mod => mod.Acronym == "NF")); AddStep("press W", () => InputManager.Key(Key.W)); + AddAssert("NF panel not selected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); + + AddStep("press Q", () => InputManager.Key(Key.Q)); AddAssert("NF panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); - AddStep("press W again", () => InputManager.Key(Key.W)); + AddStep("press Q again", () => InputManager.Key(Key.Q)); AddAssert("NF panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NF").Active.Value); AddStep("filter out everything", () => setFilter(_ => false)); @@ -171,6 +181,113 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("clear filter", () => setFilter(null)); } + [Test] + public void TestClassicKeyboardExclusiveSelection() + { + AddStep("set classic hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Classic)); + + ModColumn column = null!; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyIncrease, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyIncrease) + } + }); + + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + + AddStep("press A", () => InputManager.Key(Key.A)); + AddAssert("HR panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "HR").Active.Value); + + AddStep("press A again", () => InputManager.Key(Key.A)); + AddAssert("HR panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "HR").Active.Value); + + AddStep("press D", () => InputManager.Key(Key.D)); + AddAssert("DT panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + + AddStep("press D again", () => InputManager.Key(Key.D)); + AddAssert("DT panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press D again", () => InputManager.Key(Key.D)); + AddAssert("DT panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press Shift-D", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.D); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + AddAssert("DT panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press J", () => InputManager.Key(Key.J)); + AddAssert("no change", () => this.ChildrenOfType().Single(panel => panel.Active.Value).Mod.Acronym == "NC"); + + AddStep("filter everything but NC", () => setFilter(mod => mod.Acronym == "NC")); + + AddStep("press A", () => InputManager.Key(Key.A)); + AddAssert("no change", () => this.ChildrenOfType().Single(panel => panel.Active.Value).Mod.Acronym == "NC"); + } + + [Test] + public void TestClassicKeyboardIncompatibleSelection() + { + AddStep("set classic hotkey mode", () => configManager.SetValue(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Classic)); + + ModColumn column = null!; + AddStep("create content", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(30), + Child = column = new ModColumn(ModType.DifficultyIncrease, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AvailableMods = getExampleModsFor(ModType.DifficultyIncrease) + } + }); + + AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded); + + AddStep("press A", () => InputManager.Key(Key.A)); + AddAssert("HR panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "HR").Active.Value); + + AddStep("press A again", () => InputManager.Key(Key.A)); + AddAssert("HR panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "HR").Active.Value); + + AddStep("press D", () => InputManager.Key(Key.D)); + AddAssert("DT panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press D again", () => InputManager.Key(Key.D)); + AddAssert("DT panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel deselected", () => !this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press Shift-D", () => + { + InputManager.PressKey(Key.ShiftLeft); + InputManager.Key(Key.D); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + AddAssert("DT panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "DT").Active.Value); + AddAssert("NC panel selected", () => this.ChildrenOfType().Single(panel => panel.Mod.Acronym == "NC").Active.Value); + + AddStep("press J", () => InputManager.Key(Key.J)); + AddAssert("no change", () => this.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + + AddStep("filter everything but NC", () => setFilter(mod => mod.Acronym == "NC")); + + AddStep("press A", () => InputManager.Key(Key.A)); + AddAssert("no change", () => this.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + } + private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) @@ -181,8 +298,8 @@ namespace osu.Game.Tests.Visual.UserInterface { public new bool SelectionAnimationRunning => base.SelectionAnimationRunning; - public TestModColumn(ModType modType, bool allowBulkSelection) - : base(modType, allowBulkSelection) + public TestModColumn(ModType modType, bool allowIncompatibleSelection) + : base(modType, allowIncompatibleSelection) { } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs index 72e503dc33..181f46a996 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModDifficultyAdjustSettings.cs @@ -13,7 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Overlays.Settings; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osuTK; @@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep($"Set {name} slider to {value}", () => this.ChildrenOfType().First(c => c.LabelText == name) - .ChildrenOfType>().First().Current.Value = value); + .ChildrenOfType>().First().Current.Value = value); } private void checkBindableAtValue(string name, float? expectedValue) @@ -260,7 +260,7 @@ namespace osu.Game.Tests.Visual.UserInterface { AddAssert($"Slider {name} at {expectedValue}", () => this.ChildrenOfType().First(c => c.LabelText == name) - .ChildrenOfType>().First().Current.Value == expectedValue); + .ChildrenOfType>().First().Current.Value == expectedValue); } private void setBeatmapWithDifficultyParameters(float value) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 31061dc109..4de70f6f9e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); - AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8); + AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8); AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8); @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); - AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = m => true); + AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs index 28599c740e..bab2121d70 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; @@ -89,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddToggleStep("toggle enabled", toggle => { for (int i = 0; i < 6; i++) - button.Action = toggle ? () => { } : (Action)null; + button.Action = toggle ? () => { } : null; }); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs index 0e31a133ac..d4c2bfd422 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuButton.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; @@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddToggleStep("toggle enabled", toggle => { for (int i = 0; i < 6; i++) - button.Action = toggle ? () => { } : (Action)null; + button.Action = toggle ? () => { } : null; }); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 4c35ec40b5..ee5ef2f364 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -27,6 +27,8 @@ namespace osu.Game.Tests.Visual.UserInterface private Live first; + private const int item_count = 100; + [SetUp] public void Setup() => Schedule(() => { @@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface beatmapSets.Clear(); - for (int i = 0; i < 100; i++) + for (int i = 0; i < item_count; i++) { beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged()); } @@ -59,6 +61,13 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestRearrangeItems() { + AddUntilStep("wait for load complete", () => + { + return this + .ChildrenOfType() + .Count(i => i.ChildrenOfType().First().DelayedLoadCompleted) > 6; + }); + AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); AddStep("hold 1st item handle", () => diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 9ade9965c5..4ea7e8008a 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -26,13 +25,13 @@ namespace osu.Game.Tournament.Tests.Components FullName = { Value = "Australia" }, Players = { - new APIUser { Username = "ASecretBox" }, - new APIUser { Username = "Dereban" }, - new APIUser { Username = "mReKk" }, - new APIUser { Username = "uyghti" }, - new APIUser { Username = "Parkes" }, - new APIUser { Username = "Shiroha" }, - new APIUser { Username = "Jordan The Bear" }, + new TournamentUser { Username = "ASecretBox" }, + new TournamentUser { Username = "Dereban" }, + new TournamentUser { Username = "mReKk" }, + new TournamentUser { Username = "uyghti" }, + new TournamentUser { Username = "Parkes" }, + new TournamentUser { Username = "Shiroha" }, + new TournamentUser { Username = "Jordan The Bear" }, } }; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index f90269ae60..992baa48a3 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Tests.Components beatmap.Length = 123456; beatmap.BPM = 133; - songBar.Beatmap = beatmap; + songBar.Beatmap = new TournamentBeatmap(beatmap); }); AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock); AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime); diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 1030aae903..adfe048ce4 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -10,6 +10,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Tests.Components { @@ -32,7 +33,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap beatmap) { - Add(new TournamentBeatmapPanel(beatmap) + Add(new TournamentBeatmapPanel(new TournamentBeatmap(beatmap)) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs index 4a4def10b5..eb8b0dfbe6 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentMatchChatDisplay.cs @@ -27,16 +27,16 @@ namespace osu.Game.Tournament.Tests.Components Colour = "f2ca34" }; - private readonly APIUser redUser = new APIUser + private readonly TournamentUser redUser = new TournamentUser { Username = "BanchoBot", - Id = 3, + OnlineID = 3, }; - private readonly APIUser blueUser = new APIUser + private readonly TournamentUser blueUser = new TournamentUser { Username = "Zallius", - Id = 4, + OnlineID = 4, }; [Cached] @@ -59,11 +59,11 @@ namespace osu.Game.Tournament.Tests.Components { Team1 = { - Value = new TournamentTeam { Players = new BindableList { redUser } } + Value = new TournamentTeam { Players = new BindableList { redUser } } }, Team2 = { - Value = new TournamentTeam { Players = new BindableList { blueUser } } + Value = new TournamentTeam { Players = new BindableList { blueUser } } } }; @@ -82,19 +82,19 @@ namespace osu.Game.Tournament.Tests.Components AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { - Sender = redUser, + Sender = redUser.ToAPIUser(), Content = "I am team red." })); AddStep("message from team red", () => testChannel.AddNewMessages(new Message(nextMessageId()) { - Sender = redUser, + Sender = redUser.ToAPIUser(), Content = "I plan to win!" })); AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(nextMessageId()) { - Sender = blueUser, + Sender = blueUser.ToAPIUser(), Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." })); diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index fdd5578228..263617ddf7 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -11,6 +11,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; using osuTK; namespace osu.Game.Tournament.Tests.Components @@ -53,7 +54,7 @@ namespace osu.Game.Tournament.Tests.Components foreach (var mod in mods) { - fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym) + fillFlow.Add(new TournamentBeatmapPanel(new TournamentBeatmap(beatmap), mod.Acronym) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index d631d028bb..b440034f34 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -9,14 +9,12 @@ using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Tests.Visual; using osu.Game.Tournament.IO; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; -using osu.Game.Users; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Tournament.Tests { @@ -123,11 +121,11 @@ namespace osu.Game.Tournament.Tests }, Players = { - new APIUser { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 12 } }, - new APIUser { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 16 } }, - new APIUser { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 20 } }, - new APIUser { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 24 } }, - new APIUser { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 30 } }, + new TournamentUser { Username = "Hello", Rank = 12 }, + new TournamentUser { Username = "Hello", Rank = 16 }, + new TournamentUser { Username = "Hello", Rank = 20 }, + new TournamentUser { Username = "Hello", Rank = 24 }, + new TournamentUser { Username = "Hello", Rank = 30 }, } } }, @@ -140,11 +138,11 @@ namespace osu.Game.Tournament.Tests FullName = { Value = "United States" }, Players = { - new APIUser { Username = "Hello" }, - new APIUser { Username = "Hello" }, - new APIUser { Username = "Hello" }, - new APIUser { Username = "Hello" }, - new APIUser { Username = "Hello" }, + new TournamentUser { Username = "Hello" }, + new TournamentUser { Username = "Hello" }, + new TournamentUser { Username = "Hello" }, + new TournamentUser { Username = "Hello" }, + new TournamentUser { Username = "Hello" }, } } }, @@ -154,10 +152,10 @@ namespace osu.Game.Tournament.Tests } }; - public static APIBeatmap CreateSampleBeatmap() => - new APIBeatmap + public static TournamentBeatmap CreateSampleBeatmap() => + new TournamentBeatmap { - BeatmapSet = new APIBeatmapSet + Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 8eccb9e0e0..76d12a6b03 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components { base.Current = new Bindable(string.Empty); - ((OsuTextBox)Control).OnCommit += (sender, newText) => + ((OsuTextBox)Control).OnCommit += (sender, _) => { try { diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index 6e9c1120e4..348fd8cd76 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Components FillMode = FillMode.Fill }; - (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); + (flag = team.FlagName.GetBoundCopy()).BindValueChanged(_ => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTeamTitle.cs b/osu.Game.Tournament/Components/DrawableTeamTitle.cs index b7e936026c..e64e08a921 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitle.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitle.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Components { if (team == null) return; - (acronym = team.Acronym.GetBoundCopy()).BindValueChanged(acronym => Text.Text = team?.FullName.Value ?? string.Empty, true); + (acronym = team.Acronym.GetBoundCopy()).BindValueChanged(_ => Text.Text = team?.FullName.Value ?? string.Empty, true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 21de2bb0eb..0bb35d534c 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -7,7 +7,6 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Components }, }; - TournamentSpriteText createPlayerText(APIUser p) => + TournamentSpriteText createPlayerText(TournamentUser p) => new TournamentSpriteText { Text = p.Username, diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index 84cae92a39..eb1dde21e7 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tournament.Components { if (Team == null) return; - (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(acronym => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true); + (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(_ => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true); } } } diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index ecc655da99..a9056166be 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -14,9 +14,9 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Extensions; using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Screens.Menu; +using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -24,14 +24,14 @@ namespace osu.Game.Tournament.Components { public class SongBar : CompositeDrawable { - private APIBeatmap beatmap; + private TournamentBeatmap beatmap; public const float HEIGHT = 145 / 2f; [Resolved] private IBindable ruleset { get; set; } - public APIBeatmap Beatmap + public TournamentBeatmap Beatmap { set { diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 6416abe8a9..462743cc3d 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Components { public class TournamentBeatmapPanel : CompositeDrawable { - public readonly APIBeatmap Beatmap; + public readonly TournamentBeatmap Beatmap; private readonly string mod; @@ -31,7 +30,7 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(APIBeatmap beatmap, string mod = null) + public TournamentBeatmapPanel(TournamentBeatmap beatmap, string mod = null) { if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); @@ -61,7 +60,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - OnlineInfo = Beatmap.BeatmapSet, + OnlineInfo = Beatmap, }, new FillFlowContainer { diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 77af3e9b58..ad564c58c3 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tournament.IPC else { beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = b; + beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b); API.Queue(beatmapLookupRequest); } } diff --git a/osu.Game.Tournament/IPC/MatchIPCInfo.cs b/osu.Game.Tournament/IPC/MatchIPCInfo.cs index ef1e36f871..f438923803 100644 --- a/osu.Game.Tournament/IPC/MatchIPCInfo.cs +++ b/osu.Game.Tournament/IPC/MatchIPCInfo.cs @@ -6,13 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Legacy; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Tournament.Models; namespace osu.Game.Tournament.IPC { public class MatchIPCInfo : Component { - public Bindable Beatmap { get; } = new Bindable(); + public Bindable Beatmap { get; } = new Bindable(); public Bindable Mods { get; } = new Bindable(); public Bindable State { get; } = new Bindable(); public Bindable ChatChannel { get; } = new Bindable(); diff --git a/osu.Game.Tournament/Models/RoundBeatmap.cs b/osu.Game.Tournament/Models/RoundBeatmap.cs index 90e31ba352..65ef77e53d 100644 --- a/osu.Game.Tournament/Models/RoundBeatmap.cs +++ b/osu.Game.Tournament/Models/RoundBeatmap.cs @@ -4,7 +4,6 @@ #nullable disable using Newtonsoft.Json; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tournament.Models { @@ -14,6 +13,6 @@ namespace osu.Game.Tournament.Models public string Mods; [JsonProperty("BeatmapInfo")] - public APIBeatmap Beatmap; + public TournamentBeatmap Beatmap; } } diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs index c854488319..03beb7ca9a 100644 --- a/osu.Game.Tournament/Models/SeedingBeatmap.cs +++ b/osu.Game.Tournament/Models/SeedingBeatmap.cs @@ -5,7 +5,6 @@ using Newtonsoft.Json; using osu.Framework.Bindables; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tournament.Models { @@ -14,7 +13,7 @@ namespace osu.Game.Tournament.Models public int ID; [JsonProperty("BeatmapInfo")] - public APIBeatmap Beatmap; + public TournamentBeatmap Beatmap; public long Score; diff --git a/osu.Game.Tournament/Models/TournamentBeatmap.cs b/osu.Game.Tournament/Models/TournamentBeatmap.cs new file mode 100644 index 0000000000..274fddc490 --- /dev/null +++ b/osu.Game.Tournament/Models/TournamentBeatmap.cs @@ -0,0 +1,99 @@ +// 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.Extensions.ObjectExtensions; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; + +namespace osu.Game.Tournament.Models +{ + public class TournamentBeatmap : IBeatmapInfo, IBeatmapSetOnlineInfo + { + public int OnlineID { get; set; } + + public string DifficultyName { get; set; } = string.Empty; + + public double BPM { get; set; } + + public double Length { get; set; } + + public double StarRating { get; set; } + + public IBeatmapMetadataInfo Metadata { get; set; } = new BeatmapMetadata(); + + public IBeatmapDifficultyInfo Difficulty { get; set; } = new BeatmapDifficulty(); + + public BeatmapSetOnlineCovers Covers { get; set; } + + public TournamentBeatmap() + { + } + + public TournamentBeatmap(APIBeatmap beatmap) + { + OnlineID = beatmap.OnlineID; + DifficultyName = beatmap.DifficultyName; + BPM = beatmap.BPM; + Length = beatmap.Length; + StarRating = beatmap.StarRating; + Metadata = beatmap.Metadata; + Difficulty = beatmap.Difficulty; + Covers = beatmap.BeatmapSet.AsNonNull().Covers; + } + + public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b); + + #region IBeatmapInfo/IBeatmapSetOnlineInfo explicit implementation + + IBeatmapSetInfo IBeatmapInfo.BeatmapSet => throw new NotImplementedException(); + + string IBeatmapSetOnlineInfo.Preview => throw new NotImplementedException(); + + double IBeatmapSetOnlineInfo.BPM => throw new NotImplementedException(); + + int IBeatmapSetOnlineInfo.PlayCount => throw new NotImplementedException(); + + int IBeatmapSetOnlineInfo.FavouriteCount => throw new NotImplementedException(); + + bool IBeatmapSetOnlineInfo.HasFavourited => throw new NotImplementedException(); + + BeatmapSetOnlineAvailability IBeatmapSetOnlineInfo.Availability => throw new NotImplementedException(); + + BeatmapSetOnlineGenre IBeatmapSetOnlineInfo.Genre => throw new NotImplementedException(); + + BeatmapSetOnlineLanguage IBeatmapSetOnlineInfo.Language => throw new NotImplementedException(); + + int? IBeatmapSetOnlineInfo.TrackId => throw new NotImplementedException(); + + int[] IBeatmapSetOnlineInfo.Ratings => throw new NotImplementedException(); + + BeatmapSetHypeStatus IBeatmapSetOnlineInfo.HypeStatus => throw new NotImplementedException(); + + BeatmapSetNominationStatus IBeatmapSetOnlineInfo.NominationStatus => throw new NotImplementedException(); + + string IBeatmapInfo.Hash => throw new NotImplementedException(); + + string IBeatmapInfo.MD5Hash => throw new NotImplementedException(); + + IRulesetInfo IBeatmapInfo.Ruleset => throw new NotImplementedException(); + + DateTimeOffset IBeatmapSetOnlineInfo.Submitted => throw new NotImplementedException(); + + DateTimeOffset? IBeatmapSetOnlineInfo.Ranked => throw new NotImplementedException(); + + DateTimeOffset? IBeatmapSetOnlineInfo.LastUpdated => throw new NotImplementedException(); + + BeatmapOnlineStatus IBeatmapSetOnlineInfo.Status => throw new NotImplementedException(); + + bool IBeatmapSetOnlineInfo.HasExplicitContent => throw new NotImplementedException(); + + bool IBeatmapSetOnlineInfo.HasVideo => throw new NotImplementedException(); + + bool IBeatmapSetOnlineInfo.HasStoryboard => throw new NotImplementedException(); + + #endregion + } +} diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs index 051f1a6d80..ac57f748da 100644 --- a/osu.Game.Tournament/Models/TournamentTeam.cs +++ b/osu.Game.Tournament/Models/TournamentTeam.cs @@ -7,7 +7,6 @@ using System; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tournament.Models { @@ -38,7 +37,7 @@ namespace osu.Game.Tournament.Models { get { - int[] ranks = Players.Select(p => p.Statistics?.GlobalRank) + int[] ranks = Players.Select(p => p.Rank) .Where(i => i.HasValue) .Select(i => i.Value) .ToArray(); @@ -59,7 +58,7 @@ namespace osu.Game.Tournament.Models }; [JsonProperty] - public BindableList Players { get; set; } = new BindableList(); + public BindableList Players { get; set; } = new BindableList(); public TournamentTeam() { @@ -67,14 +66,14 @@ namespace osu.Game.Tournament.Models { // use a sane default flag name based on acronym. if (val.OldValue.StartsWith(FlagName.Value, StringComparison.InvariantCultureIgnoreCase)) - FlagName.Value = val.NewValue.Length >= 2 ? val.NewValue?.Substring(0, 2).ToUpper() : string.Empty; + FlagName.Value = val.NewValue.Length >= 2 ? val.NewValue?.Substring(0, 2).ToUpperInvariant() : string.Empty; }; FullName.ValueChanged += val => { // use a sane acronym based on full name. if (val.OldValue.StartsWith(Acronym.Value, StringComparison.InvariantCultureIgnoreCase)) - Acronym.Value = val.NewValue.Length >= 3 ? val.NewValue?.Substring(0, 3).ToUpper() : string.Empty; + Acronym.Value = val.NewValue.Length >= 3 ? val.NewValue?.Substring(0, 3).ToUpperInvariant() : string.Empty; }; } diff --git a/osu.Game.Tournament/Models/TournamentUser.cs b/osu.Game.Tournament/Models/TournamentUser.cs new file mode 100644 index 0000000000..80e58538e5 --- /dev/null +++ b/osu.Game.Tournament/Models/TournamentUser.cs @@ -0,0 +1,58 @@ +// 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 Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; + +namespace osu.Game.Tournament.Models +{ + /// + /// A tournament player user, containing simple information about the player. + /// + [Serializable] + public class TournamentUser : IUser + { + [JsonProperty(@"id")] + public int OnlineID { get; set; } + + public string Username { get; set; } = string.Empty; + + /// + /// The player's country. + /// + public Country? Country { get; set; } + + /// + /// The player's global rank, or null if not available. + /// + public int? Rank { get; set; } + + /// + /// A URL to the player's profile cover. + /// + public string CoverUrl { get; set; } = string.Empty; + + public APIUser ToAPIUser() + { + var user = new APIUser + { + Id = OnlineID, + Username = Username, + Country = Country, + CoverUrl = CoverUrl, + }; + + user.Statistics = new UserStatistics + { + User = user, + GlobalRank = Rank + }; + + return user; + } + + bool IUser.IsBot => false; + } +} diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 296267a4bc..763f576afe 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -7,9 +7,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Legacy; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; +using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens { @@ -39,7 +39,7 @@ namespace osu.Game.Tournament.Screens SongBar.Mods = mods.NewValue; } - private void beatmapChanged(ValueChangedEvent beatmap) + private void beatmapChanged(ValueChangedEvent beatmap) { SongBar.FadeInFromZero(300, Easing.OutQuint); SongBar.Beatmap = beatmap.NewValue; diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 58ba3c1e8b..32da4d1b36 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -219,7 +219,7 @@ namespace osu.Game.Tournament.Screens.Drawings } } - writeOp = writeOp?.ContinueWith(t => { writeAction(); }) ?? Task.Run(writeAction); + writeOp = writeOp?.ContinueWith(_ => { writeAction(); }) ?? Task.Run(writeAction); } private void reloadTeams() diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 6052bcdeb7..4261828df2 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tournament.Screens.Editors AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); - LadderInfo.Matches.CollectionChanged += (_, __) => updateMessage(); + LadderInfo.Matches.CollectionChanged += (_, _) => updateMessage(); updateMessage(); } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 214ae65289..1b670f4b69 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.Beatmap = res; + Model.Beatmap = new TournamentBeatmap(res); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 1377c11225..1bc929604d 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -241,7 +241,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.Beatmap = res; + Model.Beatmap = new TournamentBeatmap(res); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 005d8f36bb..11db37c8b7 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -202,14 +201,14 @@ namespace osu.Game.Tournament.Screens.Editors public void CreateNew() { - var user = new APIUser(); - team.Players.Add(user); - flow.Add(new PlayerRow(team, user)); + var player = new TournamentUser(); + team.Players.Add(player); + flow.Add(new PlayerRow(team, player)); } public class PlayerRow : CompositeDrawable { - private readonly APIUser user; + private readonly TournamentUser user; [Resolved] protected IAPIProvider API { get; private set; } @@ -217,11 +216,11 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private TournamentGameBase game { get; set; } - private readonly Bindable userId = new Bindable(); + private readonly Bindable playerId = new Bindable(); private readonly Container drawableContainer; - public PlayerRow(TournamentTeam team, APIUser user) + public PlayerRow(TournamentTeam team, TournamentUser user) { this.user = user; @@ -254,7 +253,7 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "User ID", RelativeSizeAxes = Axes.None, Width = 200, - Current = userId, + Current = playerId, }, drawableContainer = new Container { @@ -281,10 +280,10 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load() { - userId.Value = user.Id; - userId.BindValueChanged(id => + playerId.Value = user.OnlineID; + playerId.BindValueChanged(id => { - user.Id = id.NewValue ?? 0; + user.OnlineID = id.NewValue ?? 0; if (id.NewValue != id.OldValue) user.Username = string.Empty; @@ -295,13 +294,13 @@ namespace osu.Game.Tournament.Screens.Editors return; } - game.PopulateUser(user, updatePanel, updatePanel); + game.PopulatePlayer(user, updatePanel, updatePanel); }, true); } private void updatePanel() { - drawableContainer.Child = new UserGridPanel(user) { Width = 300 }; + drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 }; } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs index ac196130d6..466b9ed482 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs @@ -49,10 +49,10 @@ namespace osu.Game.Tournament.Screens.Ladder.Components }; name = round.Name.GetBoundCopy(); - name.BindValueChanged(n => textName.Text = ((losers ? "Losers " : "") + round.Name).ToUpper(), true); + name.BindValueChanged(_ => textName.Text = ((losers ? "Losers " : "") + round.Name).ToUpperInvariant(), true); description = round.Description.GetBoundCopy(); - description.BindValueChanged(n => textDescription.Text = round.Description.Value?.ToUpper(), true); + description.BindValueChanged(_ => textDescription.Text = round.Description.Value?.ToUpperInvariant(), true); } } } diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index f274503894..23bfa84afc 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tournament.Screens.Ladder foreach (var match in LadderInfo.Matches) addMatch(match); - LadderInfo.Rounds.CollectionChanged += (_, __) => layout.Invalidate(); + LadderInfo.Rounds.CollectionChanged += (_, _) => layout.Invalidate(); LadderInfo.Matches.CollectionChanged += (_, args) => { switch (args.Action) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 7ac4510ae5..5eb2142fae 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -107,7 +106,7 @@ namespace osu.Game.Tournament.Screens.MapPool ipc.Beatmap.BindValueChanged(beatmapChanged); } - private void beatmapChanged(ValueChangedEvent beatmap) + private void beatmapChanged(ValueChangedEvent beatmap) { if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) return; diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index fb33b3662a..447d6f44ce 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Screens.Setup dropdown.Items = storage.ListTournaments(); dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); - Action = () => game.GracefullyExit(); + Action = () => game.AttemptExit(); folderButton.Action = () => storage.PresentExternally(); ButtonText = "Close osu!"; diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index b9559015d6..925c697346 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -198,7 +198,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro { row.Add(new Sprite { - Texture = textures.Get($"Mods/{mods.ToLower()}"), + Texture = textures.Get($"Mods/{mods.ToLowerInvariant()}"), Scale = new Vector2(0.5f) }); } @@ -257,7 +257,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro }; foreach (var p in team.Players) - fill.Add(new RowDisplay(p.Username, p.Statistics?.GlobalRank?.ToString("\\##,0") ?? "-")); + fill.Add(new RowDisplay(p.Username, p.Rank?.ToString("\\##,0") ?? "-")); } internal class RowDisplay : CompositeDrawable diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index c2c6c271cb..537fbfc038 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -137,7 +137,7 @@ namespace osu.Game.Tournament heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; }), true); - windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => + windowMode.BindValueChanged(_ => ScheduleAfterChildren(() => { windowMode.Value = WindowMode.Windowed; }), true); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2d6d5a01e4..75c9f17d4c 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -22,7 +22,6 @@ using osu.Game.Tournament.IO; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osuTK.Input; -using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Tournament { @@ -63,7 +62,7 @@ namespace osu.Game.Tournament dependencies.Cache(new TournamentVideoResourceStore(storage)); - Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); + Textures.AddTextureSource(new TextureLoaderStore(new StorageBackedResourceStore(storage))); dependencies.CacheAs(new StableInfo(storage)); } @@ -187,9 +186,7 @@ namespace osu.Game.Tournament { var playersRequiringPopulation = ladder.Teams .SelectMany(t => t.Players) - .Where(p => string.IsNullOrEmpty(p.Username) - || p.Statistics?.GlobalRank == null - || p.Statistics?.CountryRank == null).ToList(); + .Where(p => string.IsNullOrEmpty(p.Username) || p.Rank == null).ToList(); if (playersRequiringPopulation.Count == 0) return false; @@ -197,7 +194,7 @@ namespace osu.Game.Tournament for (int i = 0; i < playersRequiringPopulation.Count; i++) { var p = playersRequiringPopulation[i]; - PopulateUser(p, immediate: true); + PopulatePlayer(p, immediate: true); updateLoadProgressMessage($"Populating user stats ({i} / {playersRequiringPopulation.Count})"); } @@ -211,7 +208,7 @@ namespace osu.Game.Tournament { var beatmapsRequiringPopulation = ladder.Rounds .SelectMany(r => r.Beatmaps) - .Where(b => string.IsNullOrEmpty(b.Beatmap?.BeatmapSet?.Title) && b.ID > 0).ToList(); + .Where(b => b.Beatmap?.OnlineID == 0 && b.ID > 0).ToList(); if (beatmapsRequiringPopulation.Count == 0) return false; @@ -222,7 +219,7 @@ namespace osu.Game.Tournament var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); API.Perform(req); - b.Beatmap = req.Response ?? new APIBeatmap(); + b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); updateLoadProgressMessage($"Populating round beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -238,7 +235,7 @@ namespace osu.Game.Tournament var beatmapsRequiringPopulation = ladder.Teams .SelectMany(r => r.SeedingResults) .SelectMany(r => r.Beatmaps) - .Where(b => string.IsNullOrEmpty(b.Beatmap?.BeatmapSet?.Title) && b.ID > 0).ToList(); + .Where(b => b.Beatmap?.OnlineID == 0 && b.ID > 0).ToList(); if (beatmapsRequiringPopulation.Count == 0) return false; @@ -249,7 +246,7 @@ namespace osu.Game.Tournament var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = b.ID }); API.Perform(req); - b.Beatmap = req.Response ?? new APIBeatmap(); + b.Beatmap = new TournamentBeatmap(req.Response ?? new APIBeatmap()); updateLoadProgressMessage($"Populating seeding beatmaps ({i} / {beatmapsRequiringPopulation.Count})"); } @@ -259,9 +256,9 @@ namespace osu.Game.Tournament private void updateLoadProgressMessage(string s) => Schedule(() => initialisationText.Text = s); - public void PopulateUser(APIUser user, Action success = null, Action failure = null, bool immediate = false) + public void PopulatePlayer(TournamentUser user, Action success = null, Action failure = null, bool immediate = false) { - var req = new GetUserRequest(user.Id, ladder.Ruleset.Value); + var req = new GetUserRequest(user.OnlineID, ladder.Ruleset.Value); if (immediate) { @@ -270,10 +267,10 @@ namespace osu.Game.Tournament } else { - req.Success += res => { populate(); }; + req.Success += _ => { populate(); }; req.Failure += _ => { - user.Id = 1; + user.OnlineID = 1; failure?.Invoke(); }; @@ -287,12 +284,12 @@ namespace osu.Game.Tournament if (res == null) return; - user.Id = res.Id; + user.OnlineID = res.Id; user.Username = res.Username; - user.Statistics = res.Statistics; + user.CoverUrl = res.CoverUrl; user.Country = res.Country; - user.Cover = res.Cover; + user.Rank = res.Statistics?.GlobalRank; success?.Invoke(); } diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index afbfc2d368..296b259d72 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -204,12 +204,12 @@ namespace osu.Game.Tournament switch (currentScreen) { - case MapPoolScreen _: + case MapPoolScreen: chatContainer.FadeIn(TournamentScreen.FADE_DELAY); chatContainer.ResizeWidthTo(1, 500, Easing.OutQuint); break; - case GameplayScreen _: + case GameplayScreen: chatContainer.FadeIn(TournamentScreen.FADE_DELAY); chatContainer.ResizeWidthTo(0.5f, 500, Easing.OutQuint); break; diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 69488277f1..ef0fa36b16 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -1,22 +1,18 @@ // 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; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; -using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets; @@ -50,31 +46,31 @@ namespace osu.Game.Beatmaps /// private readonly object bindableUpdateLock = new object(); - private CancellationTokenSource trackedUpdateCancellationSource; + private CancellationTokenSource trackedUpdateCancellationSource = new CancellationTokenSource(); [Resolved] - private BeatmapManager beatmapManager { get; set; } + private BeatmapManager beatmapManager { get; set; } = null!; [Resolved] - private Bindable currentRuleset { get; set; } + private Bindable currentRuleset { get; set; } = null!; [Resolved] - private Bindable> currentMods { get; set; } + private Bindable> currentMods { get; set; } = null!; - private ModSettingChangeTracker modSettingChangeTracker; - private ScheduledDelegate debouncedModSettingsChange; + private ModSettingChangeTracker? modSettingChangeTracker; + private ScheduledDelegate? debouncedModSettingsChange; protected override void LoadComplete() { base.LoadComplete(); - currentRuleset.BindValueChanged(_ => updateTrackedBindables()); + currentRuleset.BindValueChanged(_ => Scheduler.AddOnce(updateTrackedBindables)); currentMods.BindValueChanged(mods => { modSettingChangeTracker?.Dispose(); - updateTrackedBindables(); + Scheduler.AddOnce(updateTrackedBindables); modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); modSettingChangeTracker.SettingChanged += _ => @@ -91,9 +87,11 @@ namespace osu.Game.Beatmaps /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated). - public IBindable GetBindableDifficulty([NotNull] IBeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { - var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); + var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); + + updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken); lock (bindableUpdateLock) trackedBindables.Add(bindable); @@ -101,21 +99,6 @@ namespace osu.Game.Beatmaps return bindable; } - /// - /// Retrieves a bindable containing the star difficulty of a with a given and combination. - /// - /// - /// The bindable will not update to follow the currently-selected ruleset and mods or its settings. - /// - /// The to get the difficulty of. - /// The to get the difficulty with. If null, the 's ruleset is used. - /// The s to get the difficulty with. If null, no mods will be assumed. - /// An optional which stops updating the star difficulty for the given . - /// A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state. - public IBindable GetBindableDifficulty([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, - CancellationToken cancellationToken = default) - => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); - /// /// Retrieves the difficulty of a . /// @@ -128,8 +111,8 @@ namespace osu.Game.Beatmaps /// A return value indicates that the difficulty process failed or was interrupted early, /// and as such there is no usable star difficulty value to be returned. /// - public virtual Task GetDifficultyAsync([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo rulesetInfo = null, - [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) + public virtual Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, + IEnumerable? mods = null, CancellationToken cancellationToken = default) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; @@ -168,34 +151,6 @@ namespace osu.Game.Beatmaps updateScheduler); } - /// - /// Retrieves the that describes a star rating. - /// - /// - /// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties - /// - /// The star rating. - /// The that best describes . - public static DifficultyRating GetDifficultyRating(double starRating) - { - if (Precision.AlmostBigger(starRating, 6.5, 0.005)) - return DifficultyRating.ExpertPlus; - - if (Precision.AlmostBigger(starRating, 5.3, 0.005)) - return DifficultyRating.Expert; - - if (Precision.AlmostBigger(starRating, 4.0, 0.005)) - return DifficultyRating.Insane; - - if (Precision.AlmostBigger(starRating, 2.7, 0.005)) - return DifficultyRating.Hard; - - if (Precision.AlmostBigger(starRating, 2.0, 0.005)) - return DifficultyRating.Normal; - - return DifficultyRating.Easy; - } - /// /// Updates all tracked using the current ruleset and mods. /// @@ -204,7 +159,6 @@ namespace osu.Game.Beatmaps lock (bindableUpdateLock) { cancelTrackedBindableUpdate(); - trackedUpdateCancellationSource = new CancellationTokenSource(); foreach (var b in trackedBindables) { @@ -223,35 +177,16 @@ namespace osu.Game.Beatmaps { lock (bindableUpdateLock) { - trackedUpdateCancellationSource?.Cancel(); - trackedUpdateCancellationSource = null; + trackedUpdateCancellationSource.Cancel(); + trackedUpdateCancellationSource = new CancellationTokenSource(); - if (linkedCancellationSources != null) - { - foreach (var c in linkedCancellationSources) - c.Dispose(); + foreach (var c in linkedCancellationSources) + c.Dispose(); - linkedCancellationSources.Clear(); - } + linkedCancellationSources.Clear(); } } - /// - /// Creates a new and triggers an initial value update. - /// - /// The that star difficulty should correspond to. - /// The initial to get the difficulty with. - /// The initial s to get the difficulty with. - /// An optional which stops updating the star difficulty for the given . - /// The . - private BindableStarDifficulty createBindable([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods, - CancellationToken cancellationToken) - { - var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); - updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken); - return bindable; - } - /// /// Updates the value of a with a given ruleset + mods. /// @@ -259,7 +194,7 @@ namespace osu.Game.Beatmaps /// The to update with. /// The s to update with. /// A token that may be used to cancel this update. - private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] IRulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) + private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable? mods, CancellationToken cancellationToken = default) { // GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available // (contrary to GetAsync) @@ -329,7 +264,7 @@ namespace osu.Game.Beatmaps modSettingChangeTracker?.Dispose(); cancelTrackedBindableUpdate(); - updateScheduler?.Dispose(); + updateScheduler.Dispose(); } public readonly struct DifficultyCacheLookup : IEquatable @@ -339,7 +274,7 @@ namespace osu.Game.Beatmaps public readonly Mod[] OrderedMods; - public DifficultyCacheLookup([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) + public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo? ruleset, IEnumerable? mods) { BeatmapInfo = beatmapInfo; // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index e463492e2b..d89541844b 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -18,7 +18,6 @@ using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Models; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; @@ -49,7 +48,7 @@ namespace osu.Game.Beatmaps protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default) { if (archive != null) - beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm)); + beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm)); foreach (BeatmapInfo b in beatmapSet.Beatmaps) { @@ -177,8 +176,17 @@ namespace osu.Game.Beatmaps } Beatmap beatmap; + using (var stream = new LineBufferedReader(reader.GetStream(mapName))) + { + if (stream.PeekLine() == null) + { + Logger.Log($"No content found in first .osu file of beatmap archive ({reader.Name} / {mapName})", LoggingTarget.Database); + return null; + } + beatmap = Decoder.GetDecoder(stream).Decode(stream); + } return new BeatmapSetInfo { @@ -191,23 +199,32 @@ namespace osu.Game.Beatmaps /// /// Create all required s for the provided archive. /// - private List createBeatmapDifficulties(IList files, Realm realm) + private List createBeatmapDifficulties(BeatmapSetInfo beatmapSet, Realm realm) { var beatmaps = new List(); - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + foreach (var file in beatmapSet.Files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) { using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.GetStoragePath()))) // we need a memory stream so we can seek { IBeatmap decoded; + using (var lineReader = new LineBufferedReader(memoryStream, true)) + { + if (lineReader.PeekLine() == null) + { + LogForModel(beatmapSet, $"No content found in beatmap file {file.Filename}."); + continue; + } + decoded = Decoder.GetDecoder(lineReader).Decode(lineReader); + } string hash = memoryStream.ComputeSHA2Hash(); if (beatmaps.Any(b => b.Hash == hash)) { - Logger.Log($"Skipping import of {file.Filename} due to duplicate file content.", LoggingTarget.Database); + LogForModel(beatmapSet, $"Skipping import of {file.Filename} due to duplicate file content."); continue; } @@ -218,7 +235,7 @@ namespace osu.Game.Beatmaps if (ruleset?.Available != true) { - Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.Ruleset.OnlineID}.", LoggingTarget.Database); + LogForModel(beatmapSet, $"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.Ruleset.OnlineID}."); continue; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1dd3f82426..670dba14ec 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps foreach (BeatmapInfo b in beatmapSet.Beatmaps) b.BeatmapSet = beatmapSet; - var imported = beatmapImporter.Import(beatmapSet); + var imported = beatmapImporter.ImportModel(beatmapSet); if (imported == null) throw new InvalidOperationException("Failed to import new beatmap"); @@ -409,11 +409,8 @@ namespace osu.Game.Beatmaps public Task?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => beatmapImporter.Import(task, batchImport, cancellationToken); - public Task?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) => - beatmapImporter.Import(archive, batchImport, cancellationToken); - public Live? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) => - beatmapImporter.Import(item, archive, false, cancellationToken); + beatmapImporter.ImportModel(item, archive, false, cancellationToken); public IEnumerable HandledExtensions => beatmapImporter.HandledExtensions; @@ -421,22 +418,24 @@ namespace osu.Game.Beatmaps #region Implementation of IWorkingBeatmapCache - public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? importedBeatmap) + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo? beatmapInfo) { // Detached sets don't come with files. // If we seem to be missing files, now is a good time to re-fetch. - if (importedBeatmap?.BeatmapSet?.Files.Count == 0) + if (beatmapInfo?.IsManaged == true || beatmapInfo?.BeatmapSet?.Files.Count == 0) { Realm.Run(r => { - var refetch = r.Find(importedBeatmap.ID)?.Detach(); + var refetch = r.Find(beatmapInfo.ID)?.Detach(); if (refetch != null) - importedBeatmap = refetch; + beatmapInfo = refetch; }); } - return workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + Debug.Assert(beatmapInfo?.IsManaged != true); + + return workingBeatmapCache.GetWorkingBeatmap(beatmapInfo); } void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); @@ -457,9 +456,9 @@ namespace osu.Game.Beatmaps #region Implementation of IPostImports - public Action>>? PostImport + public Action>>? PresentImport { - set => beatmapImporter.PostImport = value; + set => beatmapImporter.PresentImport = value; } #endregion diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 5f06e03509..56a432aec4 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.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 Newtonsoft.Json; using osu.Game.Graphics; @@ -11,7 +9,7 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { - public abstract class ControlPoint : IComparable, IDeepCloneable + public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable { /// /// The time at which the control point takes effect. @@ -30,7 +28,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// An existing control point to compare with. /// Whether this is redundant when placed alongside . - public abstract bool IsRedundant(ControlPoint existing); + public abstract bool IsRedundant(ControlPoint? existing); /// /// Create an unbound copy of this control point. @@ -48,5 +46,20 @@ namespace osu.Game.Beatmaps.ControlPoints { Time = other.Time; } + + public sealed override bool Equals(object? obj) + => obj is ControlPoint otherControlPoint + && Equals(otherControlPoint); + + public virtual bool Equals(ControlPoint? other) + { + if (ReferenceEquals(other, null)) return false; + if (ReferenceEquals(other, this)) return true; + + return Time == other.Time; + } + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => Time.GetHashCode(); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs index 9df38b01ee..db479f0e5b 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointGroup.cs @@ -1,18 +1,16 @@ // 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; namespace osu.Game.Beatmaps.ControlPoints { - public class ControlPointGroup : IComparable + public class ControlPointGroup : IComparable, IEquatable { - public event Action ItemAdded; - public event Action ItemRemoved; + public event Action? ItemAdded; + public event Action? ItemRemoved; /// /// The time at which the control point takes effect. @@ -48,5 +46,23 @@ namespace osu.Game.Beatmaps.ControlPoints controlPoints.Remove(point); ItemRemoved?.Invoke(point); } + + public sealed override bool Equals(object? obj) + => obj is ControlPointGroup otherGroup + && Equals(otherGroup); + + public virtual bool Equals(ControlPointGroup? other) + => other != null + && Time == other.Time + && ControlPoints.SequenceEqual(other.ControlPoints); + + public override int GetHashCode() + { + HashCode hashCode = new HashCode(); + hashCode.Add(Time); + foreach (var point in controlPoints) + hashCode.Add(point); + return hashCode.ToHashCode(); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index eda7ef0bcc..4be6b5eede 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -254,12 +254,12 @@ namespace osu.Game.Beatmaps.ControlPoints switch (newPoint) { - case TimingControlPoint _: + case TimingControlPoint: // Timing points are a special case and need to be added regardless of fallback availability. existing = BinarySearch(TimingPoints, time); break; - case EffectControlPoint _: + case EffectControlPoint: existing = EffectPointAt(time); break; } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 8b3d755fb6..c199d1da59 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -1,8 +1,7 @@ // 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.Bindables; using osu.Game.Graphics; using osuTK.Graphics; @@ -12,7 +11,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// Note that going forward, this control point type should always be assigned directly to HitObjects. /// - public class DifficultyControlPoint : ControlPoint + public class DifficultyControlPoint : ControlPoint, IEquatable { public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { @@ -41,7 +40,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => SliderVelocityBindable.Value = value; } - public override bool IsRedundant(ControlPoint existing) + public override bool IsRedundant(ControlPoint? existing) => existing is DifficultyControlPoint existingDifficulty && SliderVelocity == existingDifficulty.SliderVelocity; @@ -51,5 +50,15 @@ namespace osu.Game.Beatmaps.ControlPoints base.CopyFrom(other); } + + public override bool Equals(ControlPoint? other) + => other is DifficultyControlPoint otherDifficultyControlPoint + && Equals(otherDifficultyControlPoint); + + public bool Equals(DifficultyControlPoint? other) + => base.Equals(other) + && SliderVelocity == other.SliderVelocity; + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SliderVelocity); } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 35425972da..ead07b4eaa 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -1,15 +1,14 @@ // 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.Bindables; using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { - public class EffectControlPoint : ControlPoint + public class EffectControlPoint : ControlPoint, IEquatable { public static readonly EffectControlPoint DEFAULT = new EffectControlPoint { @@ -68,7 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } - public override bool IsRedundant(ControlPoint existing) + public override bool IsRedundant(ControlPoint? existing) => !OmitFirstBarLine && existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode @@ -83,5 +82,17 @@ namespace osu.Game.Beatmaps.ControlPoints base.CopyFrom(other); } + + public override bool Equals(ControlPoint? other) + => other is EffectControlPoint otherEffectControlPoint + && Equals(otherEffectControlPoint); + + public bool Equals(EffectControlPoint? other) + => base.Equals(other) + && OmitFirstBarLine == other.OmitFirstBarLine + && ScrollSpeed == other.ScrollSpeed + && KiaiMode == other.KiaiMode; + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), OmitFirstBarLine, ScrollSpeed, KiaiMode); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index bed499ef3f..78dec67937 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -1,8 +1,7 @@ // 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.Bindables; using osu.Game.Audio; using osu.Game.Graphics; @@ -13,7 +12,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// Note that going forward, this control point type should always be assigned directly to HitObjects. /// - public class SampleControlPoint : ControlPoint + public class SampleControlPoint : ControlPoint, IEquatable { public const string DEFAULT_BANK = "normal"; @@ -73,7 +72,7 @@ namespace osu.Game.Beatmaps.ControlPoints public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo) => hitSampleInfo.With(newBank: hitSampleInfo.Bank ?? SampleBank, newVolume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume); - public override bool IsRedundant(ControlPoint existing) + public override bool IsRedundant(ControlPoint? existing) => existing is SampleControlPoint existingSample && SampleBank == existingSample.SampleBank && SampleVolume == existingSample.SampleVolume; @@ -85,5 +84,16 @@ namespace osu.Game.Beatmaps.ControlPoints base.CopyFrom(other); } + + public override bool Equals(ControlPoint? other) + => other is SampleControlPoint otherSampleControlPoint + && Equals(otherSampleControlPoint); + + public bool Equals(SampleControlPoint? other) + => base.Equals(other) + && SampleBank == other.SampleBank + && SampleVolume == other.SampleVolume; + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), SampleBank, SampleVolume); } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 922439fcb8..23d4d10fd8 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.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 osu.Framework.Bindables; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; @@ -8,7 +9,7 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { - public class TimingControlPoint : ControlPoint + public class TimingControlPoint : ControlPoint, IEquatable { /// /// The time signature at this control point. @@ -68,7 +69,7 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPM => 60000 / BeatLength; // Timing points are never redundant as they can change the time signature. - public override bool IsRedundant(ControlPoint existing) => false; + public override bool IsRedundant(ControlPoint? existing) => false; public override void CopyFrom(ControlPoint other) { @@ -77,5 +78,16 @@ namespace osu.Game.Beatmaps.ControlPoints base.CopyFrom(other); } + + public override bool Equals(ControlPoint? other) + => other is TimingControlPoint otherTimingControlPoint + && Equals(otherTimingControlPoint); + + public bool Equals(TimingControlPoint? other) + => base.Equals(other) + && TimeSignature.Equals(other.TimeSignature) + && BeatLength.Equals(other.BeatLength); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength); } } diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 9b97df906b..80af4108c7 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -102,7 +102,7 @@ namespace osu.Game.Beatmaps.Drawables // Matches osu-stable, in order to provide new users with roughly the same randomised selection of bundled beatmaps. var random = new LegacyRandom(DateTime.UtcNow.Year * 1000 + (DateTime.UtcNow.DayOfYear / 7)); - downloadableFilenames.AddRange(sourceFilenames.OrderBy(x => random.NextDouble()).Take(limit ?? int.MaxValue)); + downloadableFilenames.AddRange(sourceFilenames.OrderBy(_ => random.NextDouble()).Take(limit ?? int.MaxValue)); } catch { } } diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs index 78481ac27a..bc0fcb92bb 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons }; favouriteRequest.Failure += e => { - Logger.Error(e, $"Failed to {actionType.ToString().ToLower()} beatmap: {e.Message}"); + Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}"); Enabled.Value = true; }; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index a1b0f04aae..679e9c3665 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -1,12 +1,6 @@ -// 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. -#nullable disable - -using System; -using System.Collections.Generic; -using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -16,19 +10,17 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip + public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip, IHasCurrentValue { - private readonly Container iconContainer; - /// /// Size of this difficulty icon. /// @@ -38,57 +30,53 @@ namespace osu.Game.Beatmaps.Drawables set => iconContainer.Size = value; } - [NotNull] - private readonly IBeatmapInfo beatmapInfo; + /// + /// Whether to display a tooltip on hover. Only works if a beatmap was provided at construction time. + /// + public bool ShowTooltip { get; set; } = true; + + private readonly IBeatmapInfo? beatmap; - [CanBeNull] private readonly IRulesetInfo ruleset; - [CanBeNull] - private readonly IReadOnlyList mods; + private Drawable background = null!; - private readonly bool shouldShowTooltip; + private readonly Container iconContainer; - private readonly bool performBackgroundDifficultyLookup; + private readonly BindableWithCurrent difficulty = new BindableWithCurrent(); - private readonly Bindable difficultyBindable = new Bindable(); - - private Drawable background; - - /// - /// Creates a new with a given and combination. - /// - /// The beatmap to show the difficulty of. - /// The ruleset to show the difficulty with. - /// The mods to show the difficulty with. - /// Whether to display a tooltip when hovered. - /// Whether to perform difficulty lookup (including calculation if necessary). - public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) - : this(beatmapInfo, shouldShowTooltip, performBackgroundDifficultyLookup) + public virtual Bindable Current { - this.ruleset = ruleset ?? beatmapInfo.Ruleset; - this.mods = mods ?? Array.Empty(); - } - - /// - /// Creates a new that follows the currently-selected ruleset and mods. - /// - /// The beatmap to show the difficulty of. - /// Whether to display a tooltip when hovered. - /// Whether to perform difficulty lookup (including calculation if necessary). - public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) - { - this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo)); - this.shouldShowTooltip = shouldShowTooltip; - this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; - - AutoSizeAxes = Axes.Both; - - InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; + get => difficulty.Current; + set => difficulty.Current = value; } [Resolved] - private IRulesetStore rulesets { get; set; } + private IRulesetStore rulesets { get; set; } = null!; + + /// + /// Creates a new . Will use provided beatmap's for initial value. + /// + /// The beatmap to be displayed in the tooltip, and to be used for the initial star rating value. + /// An optional ruleset to be used for the icon display, in place of the beatmap's ruleset. + public DifficultyIcon(IBeatmapInfo beatmap, IRulesetInfo? ruleset = null) + : this(ruleset ?? beatmap.Ruleset) + { + this.beatmap = beatmap; + Current.Value = new StarDifficulty(beatmap.StarRating, 0); + } + + /// + /// Creates a new without an associated beatmap. + /// + /// The ruleset to be used for the icon display. + public DifficultyIcon(IRulesetInfo ruleset) + { + this.ruleset = ruleset; + + AutoSizeAxes = Axes.Both; + InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; + } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -111,7 +99,6 @@ namespace osu.Game.Beatmaps.Drawables Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(beatmapInfo.StarRating) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -124,17 +111,12 @@ namespace osu.Game.Beatmaps.Drawables }, }; - if (performBackgroundDifficultyLookup) - iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); - else - difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarRating, 0); - - difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); + Current.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars), true); } private Drawable getRulesetIcon() { - int? onlineID = (ruleset ?? beatmapInfo.Ruleset).OnlineID; + int? onlineID = ruleset.OnlineID; if (onlineID >= 0 && rulesets.GetRuleset(onlineID.Value)?.CreateInstance() is Ruleset rulesetInstance) return rulesetInstance.CreateIcon(); @@ -142,51 +124,10 @@ namespace osu.Game.Beatmaps.Drawables return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; } - ITooltip IHasCustomTooltip.GetCustomTooltip() => new DifficultyIconTooltip(); + ITooltip IHasCustomTooltip. + GetCustomTooltip() => new DifficultyIconTooltip(); - DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; - - private class DifficultyRetriever : Component - { - public readonly Bindable StarDifficulty = new Bindable(); - - private readonly IBeatmapInfo beatmapInfo; - private readonly IRulesetInfo ruleset; - private readonly IReadOnlyList mods; - - private CancellationTokenSource difficultyCancellation; - - [Resolved] - private BeatmapDifficultyCache difficultyCache { get; set; } - - public DifficultyRetriever(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, IReadOnlyList mods) - { - this.beatmapInfo = beatmapInfo; - this.ruleset = ruleset; - this.mods = mods; - } - - private IBindable localStarDifficulty; - - [BackgroundDependencyLoader] - private void load() - { - difficultyCancellation = new CancellationTokenSource(); - localStarDifficulty = ruleset != null - ? difficultyCache.GetBindableDifficulty(beatmapInfo, ruleset, mods, difficultyCancellation.Token) - : difficultyCache.GetBindableDifficulty(beatmapInfo, difficultyCancellation.Token); - localStarDifficulty.BindValueChanged(d => - { - if (d.NewValue is StarDifficulty diff) - StarDifficulty.Value = diff; - }); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - difficultyCancellation?.Cancel(); - } - } + DifficultyIconTooltipContent IHasCustomTooltip. + TooltipContent => (ShowTooltip && beatmap != null ? new DifficultyIconTooltipContent(beatmap, Current) : null)!; } } diff --git a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs deleted file mode 100644 index 15ca4c60d4..0000000000 --- a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; -using osuTK.Graphics; - -namespace osu.Game.Beatmaps.Drawables -{ - /// - /// A difficulty icon that contains a counter on the right-side of it. - /// - /// - /// Used in cases when there are too many difficulty icons to show. - /// - public class GroupedDifficultyIcon : DifficultyIcon - { - public GroupedDifficultyIcon(IEnumerable beatmaps, IRulesetInfo ruleset, Color4 counterColour) - : base(beatmaps.OrderBy(b => b.StarRating).Last(), ruleset, null, false) - { - AddInternal(new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Padding = new MarginPadding { Left = Size.X }, - Margin = new MarginPadding { Left = 2, Right = 5 }, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - Text = beatmaps.Count().ToString(), - Colour = counterColour, - }); - } - } -} diff --git a/osu.Game/Beatmaps/EFBeatmapInfo.cs b/osu.Game/Beatmaps/EFBeatmapInfo.cs index 34311448eb..20abdc686a 100644 --- a/osu.Game/Beatmaps/EFBeatmapInfo.cs +++ b/osu.Game/Beatmaps/EFBeatmapInfo.cs @@ -128,7 +128,7 @@ namespace osu.Game.Beatmaps public List Scores { get; set; } [JsonIgnore] - public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating); + public DifficultyRating DifficultyRating => StarDifficulty.GetDifficultyRating(StarRating); public override string ToString() => this.GetDisplayTitle(); diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index aad29f46fb..ca1bcc97fd 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Formats } if (line == null) - throw new IOException("Unknown file format (null)"); + throw new IOException("Unknown file format (no content)"); var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).Select(d => d.Value).FirstOrDefault(); diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index 2f11c18993..4f292a9a1f 100644 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs @@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps.Formats { public static void Register() { - AddDecoder("{", m => new JsonBeatmapDecoder()); + AddDecoder("{", _ => new JsonBeatmapDecoder()); } protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 984e88d3a9..03c63ff4f2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -373,11 +373,11 @@ namespace osu.Game.Beatmaps.Formats switch (hitObject) { - case IHasPath _: + case IHasPath: type |= LegacyHitObjectType.Slider; break; - case IHasDuration _: + case IHasDuration: if (onlineRulesetID == 3) type |= LegacyHitObjectType.Hold; else diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index f2c5b494de..a5e6ac0a1c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.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 osu.Framework.Extensions; @@ -145,7 +143,7 @@ namespace osu.Game.Beatmaps.Formats protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath(); - protected enum Section + public enum Section { General, Editor, @@ -162,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats } [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")] - public class LegacyDifficultyControlPoint : DifficultyControlPoint + public class LegacyDifficultyControlPoint : DifficultyControlPoint, IEquatable { /// /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. @@ -188,9 +186,20 @@ namespace osu.Game.Beatmaps.Formats BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier; } + + public override bool Equals(ControlPoint? other) + => other is LegacyDifficultyControlPoint otherLegacyDifficultyControlPoint + && Equals(otherLegacyDifficultyControlPoint); + + public bool Equals(LegacyDifficultyControlPoint? other) + => base.Equals(other) + && BpmMultiplier == other.BpmMultiplier; + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), BpmMultiplier); } - internal class LegacySampleControlPoint : SampleControlPoint + internal class LegacySampleControlPoint : SampleControlPoint, IEquatable { public int CustomSampleBank; @@ -204,7 +213,7 @@ namespace osu.Game.Beatmaps.Formats return baseInfo; } - public override bool IsRedundant(ControlPoint existing) + public override bool IsRedundant(ControlPoint? existing) => base.IsRedundant(existing) && existing is LegacySampleControlPoint existingSample && CustomSampleBank == existingSample.CustomSampleBank; @@ -215,6 +224,17 @@ namespace osu.Game.Beatmaps.Formats CustomSampleBank = ((LegacySampleControlPoint)other).CustomSampleBank; } + + public override bool Equals(ControlPoint? other) + => other is LegacySampleControlPoint otherLegacySampleControlPoint + && Equals(otherLegacySampleControlPoint); + + public bool Equals(LegacySampleControlPoint? other) + => base.Equals(other) + && CustomSampleBank == other.CustomSampleBank; + + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank); } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 3021688aaf..b8f60f0bc6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Formats { // note that this isn't completely correct AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder(Parsing.ParseInt(m.Split('v').Last()))); - AddDecoder(@"[Events]", m => new LegacyStoryboardDecoder()); + AddDecoder(@"[Events]", _ => new LegacyStoryboardDecoder()); SetFallbackDecoder(() => new LegacyStoryboardDecoder()); } diff --git a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs index 160b7cf0ae..e1634e7d24 100644 --- a/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapOnlineInfo.cs @@ -13,25 +13,50 @@ namespace osu.Game.Beatmaps /// int? MaxCombo { get; } + /// + /// The approach rate. + /// + float ApproachRate { get; } + + /// + /// The circle size. + /// + float CircleSize { get; } + + /// + /// The drain rate. + /// + float DrainRate { get; } + + /// + /// The overall difficulty. + /// + float OverallDifficulty { get; } + /// /// The amount of circles in this beatmap. /// - public int CircleCount { get; } + int CircleCount { get; } /// /// The amount of sliders in this beatmap. /// - public int SliderCount { get; } + int SliderCount { get; } + + /// + /// The amount of spinners in tihs beatmap. + /// + int SpinnerCount { get; } /// /// The amount of plays this beatmap has. /// - public int PlayCount { get; } + int PlayCount { get; } /// /// The amount of passes this beatmap has. /// - public int PassCount { get; } + int PassCount { get; } APIFailTimes? FailTimes { get; } } diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index 19432f0c58..5c3c72c9e4 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -56,12 +56,12 @@ namespace osu.Game.Beatmaps.Legacy { switch (newPoint) { - case SampleControlPoint _: + case SampleControlPoint: // intentionally don't use SamplePointAt (we always need to consider the first sample point). var existing = BinarySearch(SamplePoints, time); return newPoint.IsRedundant(existing); - case DifficultyControlPoint _: + case DifficultyControlPoint: return newPoint.IsRedundant(DifficultyPointAt(time)); default: diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs index 91bc3aacf6..e042f1c698 100644 --- a/osu.Game/Beatmaps/StarDifficulty.cs +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -4,6 +4,7 @@ #nullable disable using JetBrains.Annotations; +using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Beatmaps @@ -50,6 +51,34 @@ namespace osu.Game.Beatmaps Attributes = null; } - public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(Stars); + public DifficultyRating DifficultyRating => GetDifficultyRating(Stars); + + /// + /// Retrieves the that describes a star rating. + /// + /// + /// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties + /// + /// The star rating. + /// The that best describes . + public static DifficultyRating GetDifficultyRating(double starRating) + { + if (Precision.AlmostBigger(starRating, 6.5, 0.005)) + return DifficultyRating.ExpertPlus; + + if (Precision.AlmostBigger(starRating, 5.3, 0.005)) + return DifficultyRating.Expert; + + if (Precision.AlmostBigger(starRating, 4.0, 0.005)) + return DifficultyRating.Insane; + + if (Precision.AlmostBigger(starRating, 2.7, 0.005)) + return DifficultyRating.Hard; + + if (Precision.AlmostBigger(starRating, 2.0, 0.005)) + return DifficultyRating.Normal; + + return DifficultyRating.Easy; + } } } diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs index 23b2ef60dd..742d757bec 100644 --- a/osu.Game/Collections/BeatmapCollection.cs +++ b/osu.Game/Collections/BeatmapCollection.cs @@ -36,7 +36,7 @@ namespace osu.Game.Collections public BeatmapCollection() { - BeatmapHashes.CollectionChanged += (_, __) => onChange(); + BeatmapHashes.CollectionChanged += (_, _) => onChange(); Name.ValueChanged += _ => onChange(); } diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 46d90b930c..d099eb6e1b 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -67,7 +67,7 @@ namespace osu.Game.Collections // An extra bindable is enough to subvert this behaviour. base.Current = Current; - collections.BindCollectionChanged((_, __) => collectionsChanged(), true); + collections.BindCollectionChanged((_, _) => collectionsChanged(), true); Current.BindValueChanged(filterChanged, true); } @@ -233,7 +233,7 @@ namespace osu.Game.Collections if (collectionBeatmaps != null) { - collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); + collectionBeatmaps.CollectionChanged += (_, _) => collectionChanged(); beatmap.BindValueChanged(_ => collectionChanged(), true); } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index c6e52a1b75..796b3c426c 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -261,7 +261,7 @@ namespace osu.Game.Collections private void backgroundSave() { int current = Interlocked.Increment(ref lastSave); - Task.Delay(100).ContinueWith(task => + Task.Delay(100).ContinueWith(_ => { if (current != lastSave) return; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a0e1d9ddc4..713166a9a0 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -20,6 +20,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Overlays; +using osu.Game.Overlays.Mods.Input; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; @@ -47,6 +48,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title); SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); + SetDefault(OsuSetting.ModSelectHotkeyStyle, ModSelectHotkeyStyle.Sequential); SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); @@ -324,6 +326,7 @@ namespace osu.Game.Configuration SongSelectGroupingMode, SongSelectSortingMode, RandomSelectAlgorithm, + ModSelectHotkeyStyle, ShowFpsDisplay, ChatDisplayHeight, BeatmapListingCardSize, diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 8af649fbfa..af91fb4971 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -209,6 +209,10 @@ namespace osu.Game.Database public void SetMigrationCompletion() => migrationComplete.Set(); - public void WaitForMigrationCompletion() => migrationComplete.Wait(); + public void WaitForMigrationCompletion() + { + if (!migrationComplete.Wait(300000)) + throw new TimeoutException("Migration took too long (likely stuck)."); + } } } diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index bf9f2f9c75..ebb8be39ef 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.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.Threading.Tasks; using osu.Game.Overlays.Notifications; @@ -11,7 +12,7 @@ namespace osu.Game.Database /// A class which handles importing of associated models to the game store. /// /// The model type. - public interface IModelImporter : IPostNotifications, IPostImports, ICanAcceptFiles + public interface IModelImporter : IPostNotifications, ICanAcceptFiles where TModel : class, IHasGuidPrimaryKey { /// @@ -25,6 +26,11 @@ namespace osu.Game.Database /// /// A user displayable name for the model type associated with this manager. /// - string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLowerInvariant()}"; + + /// + /// Fired when the user requests to view the resulting import. + /// + public Action>>? PresentImport { set; } } } diff --git a/osu.Game/Database/IPostImports.cs b/osu.Game/Database/IPostImports.cs deleted file mode 100644 index 83a211f6e6..0000000000 --- a/osu.Game/Database/IPostImports.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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; - -namespace osu.Game.Database -{ - public interface IPostImports - where TModel : class, IHasGuidPrimaryKey - { - /// - /// Fired when the user requests to view the resulting import. - /// - public Action>>? PostImport { set; } - } -} diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index 0643300fc5..e7f599d85f 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.cs @@ -33,7 +33,7 @@ namespace osu.Game.Database } /// - /// Construct a new import task from a stream. + /// Construct a new import task from a stream. The provided stream will be disposed after reading. /// public ImportTask(Stream stream, string filename) { @@ -62,6 +62,7 @@ namespace osu.Game.Database { // This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out). memoryStream = new MemoryStream(stream.ReadAllBytesToArray()); + stream.Dispose(); } if (ZipUtils.IsZipArchive(memoryStream)) diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index e0c3dccf57..4224c92f2c 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using osu.Framework.Platform; @@ -46,6 +47,7 @@ namespace osu.Game.Database Realm.Realm.Write(realm => { var managed = realm.Find(item.ID); + Debug.Assert(managed != null); operation(managed); item.Files.Clear(); @@ -201,6 +203,6 @@ namespace osu.Game.Database public Action? PostNotification { get; set; } - public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLowerInvariant()}"; } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 865a8b5021..cb34a92702 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -102,6 +102,12 @@ namespace osu.Game.Database private Realm? updateRealm; + /// + /// Tracks whether a realm was ever fetched from this instance. + /// After a fetch occurs, blocking operations will be guaranteed to restore any subscriptions. + /// + private bool hasInitialisedOnce; + private bool isSendingNotificationResetEvents; public Realm Realm => ensureUpdateRealm(); @@ -121,11 +127,12 @@ namespace osu.Game.Database if (updateRealm == null) { updateRealm = getRealmInstance(); + hasInitialisedOnce = true; Logger.Log(@$"Opened realm ""{updateRealm.Config.DatabasePath}"" at version {updateRealm.Config.SchemaVersion}"); // Resubscribe any subscriptions - foreach (var action in customSubscriptionsResetMap.Keys) + foreach (var action in customSubscriptionsResetMap.Keys.ToArray()) registerSubscription(action); } @@ -385,11 +392,22 @@ namespace osu.Game.Database /// Write changes to realm asynchronously, guaranteeing order of execution. /// /// The work to run. - public async Task WriteAsync(Action action) + public Task WriteAsync(Action action) { - total_writes_async.Value++; - using (var realm = getRealmInstance()) - await realm.WriteAsync(() => action(realm)); + // Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval. + // Adding a forced Task.Run resolves this. + + return Task.Run(async () => + { + total_writes_async.Value++; + + // Not attempting to use Realm.GetInstanceAsync as there's seemingly no benefit to us (for now) and it adds complexity due to locking + // concerns in getRealmInstance(). On a quick check, it looks to be more suited to cases where realm is connecting to an online sync + // server, which we don't use. May want to report upstream or revisit in the future. + using (var realm = getRealmInstance()) + // ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]). + await realm.WriteAsync(() => action(realm)); + }); } /// @@ -443,7 +461,7 @@ namespace osu.Game.Database public IDisposable SubscribeToPropertyChanged(Func modelAccessor, Expression> propertyLookup, Action onChanged) where TModel : RealmObjectBase { - return RegisterCustomSubscription(r => + return RegisterCustomSubscription(_ => { string propertyName = getMemberName(propertyLookup); @@ -795,13 +813,7 @@ namespace osu.Game.Database lock (realmLock) { - if (updateRealm == null) - { - // null realm means the update thread has not yet retrieved its instance. - // we don't need to worry about reviving the update instance in this case, so don't bother with the SynchronizationContext. - Debug.Assert(!ThreadSafety.IsUpdateThread); - } - else + if (hasInitialisedOnce) { if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread."); @@ -811,17 +823,17 @@ namespace osu.Game.Database // Before disposing the update context, clean up all subscriptions. // Note that in the case of realm notification subscriptions, this is not really required (they will be cleaned up by disposal). // In the case of custom subscriptions, we want them to fire before the update realm is disposed in case they do any follow-up work. - foreach (var action in customSubscriptionsResetMap) + foreach (var action in customSubscriptionsResetMap.ToArray()) { action.Value?.Dispose(); customSubscriptionsResetMap[action.Key] = null; } + + updateRealm?.Dispose(); + updateRealm = null; } Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - - updateRealm?.Dispose(); - updateRealm = null; } const int sleep_length = 200; diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index c5e04a8a0f..76f6db1384 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -56,7 +56,7 @@ namespace osu.Game.Database /// private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter)); - public virtual IEnumerable HandledExtensions => new[] { @".zip" }; + public abstract IEnumerable HandledExtensions { get; } protected readonly RealmFileStore Files; @@ -65,7 +65,7 @@ namespace osu.Game.Database /// /// Fired when the user requests to view the resulting import. /// - public Action>>? PostImport { get; set; } + public Action>>? PresentImport { get; set; } /// /// Set an endpoint for notifications to be posted to. @@ -158,12 +158,12 @@ namespace osu.Game.Database ? $"Imported {imported.First().GetDisplayString()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; - if (imported.Count > 0 && PostImport != null) + if (imported.Count > 0 && PresentImport != null) { notification.CompletionText += " Click to view."; notification.CompletionClickAction = () => { - PostImport?.Invoke(imported); + PresentImport?.Invoke(imported); return true; }; } @@ -188,7 +188,7 @@ namespace osu.Game.Database Live? import; using (ArchiveReader reader = task.GetReader()) - import = await Import(reader, batchImport, cancellationToken).ConfigureAwait(false); + import = await importFromArchive(reader, batchImport, cancellationToken).ConfigureAwait(false); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with items from default storage. @@ -208,12 +208,15 @@ namespace osu.Game.Database } /// - /// Silently import an item from an . + /// Create and import a model based off the provided . /// + /// + /// This method also handled queueing the import task on a relevant import thread pool. + /// /// The archive to be imported. /// Whether this import is part of a larger batch. /// An optional cancellation token. - public async Task?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) + private async Task?> importFromArchive(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -236,7 +239,7 @@ namespace osu.Game.Database return null; } - var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, batchImport, cancellationToken), + var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, batchImport, cancellationToken), cancellationToken, TaskCreationOptions.HideScheduler, batchImport ? import_scheduler_batch : import_scheduler); @@ -251,7 +254,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// If true, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model. /// An optional cancellation token. - public virtual Live? Import(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm => + public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm => { cancellationToken.ThrowIfCancellationRequested(); @@ -293,7 +296,8 @@ namespace osu.Game.Database try { - LogForModel(item, @"Beginning import..."); + // Log output here will be missing a valid hash in non-batch imports. + LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}..."); // TODO: do we want to make the transaction this local? not 100% sure, will need further investigation. using (var transaction = realm.BeginWrite()) @@ -335,6 +339,8 @@ namespace osu.Game.Database transaction.Commit(); } + PostImport(item, realm); + LogForModel(item, @"Import successfully completed!"); } catch (Exception e) @@ -470,6 +476,15 @@ namespace osu.Game.Database { } + /// + /// Perform any final actions after the import has been committed to the database. + /// + /// The model prepared for import. + /// The current realm context. + protected virtual void PostImport(TModel model, Realm realm) + { + } + /// /// Check whether an existing model already exists for a new import item. /// @@ -535,6 +550,6 @@ namespace osu.Game.Database yield return f.Filename; } - public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLowerInvariant()}"; } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index 25416c7fa2..a771aa04df 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -20,7 +20,7 @@ namespace osu.Game.Database { private static readonly IMapper write_mapper = new MapperConfiguration(c => { - c.ShouldMapField = fi => false; + c.ShouldMapField = _ => false; c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true; c.CreateMap() @@ -70,7 +70,7 @@ namespace osu.Game.Database } }); - c.Internal().ForAllMaps((typeMap, expression) => + c.Internal().ForAllMaps((_, expression) => { expression.ForAllMembers(m => { @@ -87,7 +87,7 @@ namespace osu.Game.Database c.CreateMap() .ConstructUsing(_ => new BeatmapSetInfo(null)) .MaxDepth(2) - .AfterMap((s, d) => + .AfterMap((_, d) => { foreach (var beatmap in d.Beatmaps) beatmap.BeatmapSet = d; @@ -97,7 +97,7 @@ namespace osu.Game.Database // Only hasn't been done yet as we detach at the point of BeatmapInfo less often. c.CreateMap() .MaxDepth(2) - .AfterMap((s, d) => + .AfterMap((_, d) => { for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++) { @@ -121,7 +121,7 @@ namespace osu.Game.Database .ConstructUsing(_ => new BeatmapSetInfo(null)) .MaxDepth(2) .ForMember(b => b.Files, cc => cc.Ignore()) - .AfterMap((s, d) => + .AfterMap((_, d) => { foreach (var beatmap in d.Beatmaps) beatmap.BeatmapSet = d; @@ -135,14 +135,14 @@ namespace osu.Game.Database private static void applyCommonConfiguration(IMapperConfigurationExpression c) { - c.ShouldMapField = fi => false; + c.ShouldMapField = _ => false; // This is specifically to avoid mapping explicit interface implementations. // If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>. // Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist c.ShouldMapProperty = pi => pi.GetMethod?.IsPublic == true; - c.Internal().ForAllMaps((typeMap, expression) => + c.Internal().ForAllMaps((_, expression) => { expression.ForAllMembers(m => { diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index 5165ae59b5..b4a0c02e35 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -31,7 +31,7 @@ namespace osu.Game.Extensions { var tcs = new TaskCompletionSource(); - task.ContinueWith(t => + task.ContinueWith(_ => { // the previous task has finished execution or been cancelled, so we can run the provided continuation. diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 4146bbcc5e..45a935d165 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -127,7 +127,7 @@ namespace osu.Game.Graphics.Containers TimeSinceLastBeat = beatLength - TimeUntilNextBeat; - if (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. diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index a67e5ae66f..bf96695fd3 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Game.Online; using osu.Game.Users; namespace osu.Game.Graphics.Containers @@ -25,7 +26,7 @@ namespace osu.Game.Graphics.Containers } [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + private ILinkHandler linkHandler { get; set; } [Resolved] private GameHost host { get; set; } @@ -81,8 +82,8 @@ namespace osu.Game.Graphics.Containers { if (action != null) action(); - else if (game != null) - game.HandleLink(link); + else if (linkHandler != null) + linkHandler.HandleLink(link); // fallback to handle cases where OsuGame is not available, ie. tournament client. else if (link.Action == LinkAction.External) host.OpenUrlExternally(link.Argument.ToString()); diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index aa156efee9..a2370a76cb 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers.Markdown { switch (markdownObject) { - case YamlFrontMatterBlock _: + case YamlFrontMatterBlock: // Don't parse YAML Frontmatter break; diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownLinkText.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownLinkText.cs index dca9e3de6e..3a16eb0a0f 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownLinkText.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownLinkText.cs @@ -8,6 +8,7 @@ using Markdig.Syntax.Inlines; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers.Markdown; +using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Overlays; @@ -16,7 +17,7 @@ namespace osu.Game.Graphics.Containers.Markdown public class OsuMarkdownLinkText : MarkdownLinkText { [Resolved(canBeNull: true)] - private OsuGame game { get; set; } + private ILinkHandler linkHandler { get; set; } private readonly string text; private readonly string title; @@ -51,7 +52,7 @@ namespace osu.Game.Graphics.Containers.Markdown }; } - protected override void OnLinkPressed() => game?.HandleLink(Url); + protected override void OnLinkPressed() => linkHandler?.HandleLink(Url); private class OsuMarkdownLinkCompiler : DrawableLinkCompiler { diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs index 84d0d1eb32..b604ae73eb 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs @@ -3,9 +3,14 @@ #nullable disable +using System.Collections.Specialized; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; namespace osu.Game.Graphics.Containers { @@ -18,11 +23,49 @@ namespace osu.Game.Graphics.Containers protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + private Sample sampleSwap; + private double sampleLastPlaybackTime; + protected sealed override RearrangeableListItem CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d => { d.DragActive.BindTo(DragActive); }); protected abstract OsuRearrangeableListItem CreateOsuDrawable(TModel item); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Items.CollectionChanged += (_, args) => + { + if (args.Action == NotifyCollectionChangedAction.Move) + playSwapSample(); + }; + } + + private void playSwapSample() + { + if (!DragActive.Value) + return; + + if (Time.Current - sampleLastPlaybackTime <= 35) + return; + + var channel = sampleSwap?.GetChannel(); + if (channel == null) + return; + + channel.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + channel.Play(); + sampleLastPlaybackTime = Time.Current; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleSwap = audio.Samples.Get(@"UI/item-swap"); + sampleLastPlaybackTime = Time.Current; + } } } diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index 5c8579a144..17c51129a7 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -260,7 +260,7 @@ namespace osu.Game.Graphics.Containers if (host.Window == null) return; bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero; - host.Window.CursorConfineRect = coversWholeScreen ? (RectangleF?)null : ToScreenSpace(DrawRectangle).AABBFloat; + host.Window.CursorConfineRect = coversWholeScreen ? null : ToScreenSpace(DrawRectangle).AABBFloat; } } } diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 4544633318..10ed76ebdd 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -51,12 +51,16 @@ namespace osu.Game.Graphics.Cursor { if (dragRotationState != DragRotationState.NotDragging) { + // make the rotation centre point floating. + if (Vector2.Distance(positionMouseDown, e.MousePosition) > 60) + positionMouseDown = Interpolation.ValueAt(0.005f, positionMouseDown, e.MousePosition, 0, Clock.ElapsedFrameTime); + var position = e.MousePosition; float distance = Vector2Extensions.Distance(position, positionMouseDown); // don't start rotating until we're moved a minimum distance away from the mouse down location, // else it can have an annoying effect. - if (dragRotationState == DragRotationState.DragStarted && distance > 30) + if (dragRotationState == DragRotationState.DragStarted && distance > 80) dragRotationState = DragRotationState.Rotating; // don't rotate when distance is zero to avoid NaN @@ -71,7 +75,7 @@ namespace osu.Game.Graphics.Cursor if (diff > 180) diff -= 360; degrees = activeCursor.Rotation + diff; - activeCursor.RotateTo(degrees, 600, Easing.OutQuint); + activeCursor.RotateTo(degrees, 120, Easing.OutQuint); } } @@ -111,7 +115,7 @@ namespace osu.Game.Graphics.Cursor if (dragRotationState != DragRotationState.NotDragging) { - activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); + activeCursor.RotateTo(0, 400 * (0.5f + Math.Abs(activeCursor.Rotation / 960)), Easing.OutElasticQuarter); dragRotationState = DragRotationState.NotDragging; } diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 7147f89dd0..2cc9e63c87 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -103,7 +103,9 @@ namespace osu.Game.Graphics framesWaitedEvent.Set(); }, 10, true); - framesWaitedEvent.Wait(); + if (!framesWaitedEvent.Wait(1000)) + throw new TimeoutException("Screenshot data did not arrive in a timely fashion"); + waitDelegate.Cancel(); } } diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 00ed6c1c92..f55875ac58 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterface } //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards - RemoveRange(Children.Where((bar, index) => index >= value.Count()).ToList()); + RemoveRange(Children.Where((_, index) => index >= value.Count()).ToList()); } } } diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 407823fc2a..b3655eaab4 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -55,12 +55,12 @@ namespace osu.Game.Graphics.UserInterface switch (e) { // blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer. - case ScrollEvent _: + case ScrollEvent: return false; // blocking touch events causes the ISourcedFromTouch versions to not be fired, potentially impeding behaviour of drawables *above* the loading layer that may utilise these. // note that this will not work well if touch handling elements are beneath this loading layer (something to consider for the future). - case TouchEvent _: + case TouchEvent: return false; } diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index 50682ddfe0..0bbcb2b976 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -1,7 +1,6 @@ // 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.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -47,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { - Active.BindDisabledChanged(disabled => Action = disabled ? (Action?)null : Active.Toggle, true); + Active.BindDisabledChanged(disabled => Action = disabled ? null : Active.Toggle, true); Active.BindValueChanged(_ => { updateActiveState(); diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index a8051a94d9..d9274bf2cb 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -68,7 +68,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), - ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => CreateStar()) + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(_ => CreateStar()) } }; } diff --git a/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs b/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs index 9d1650c970..6787e17113 100644 --- a/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/ToggleMenuItem.cs @@ -34,6 +34,6 @@ namespace osu.Game.Graphics.UserInterface { } - public override IconUsage? GetIconForState(bool state) => state ? (IconUsage?)FontAwesome.Solid.Check : null; + public override IconUsage? GetIconForState(bool state) => state ? FontAwesome.Solid.Check : null; } } diff --git a/osu.Game/IPC/IPCTimeoutException.cs b/osu.Game/IPC/IPCTimeoutException.cs new file mode 100644 index 0000000000..d820184468 --- /dev/null +++ b/osu.Game/IPC/IPCTimeoutException.cs @@ -0,0 +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; + +namespace osu.Game.IPC +{ + public class IPCTimeoutException : TimeoutException + { + public IPCTimeoutException(Type channelType) + : base($@"IPC took too long to send message via channel {channelType}") + { + } + } +} diff --git a/osu.Game/IPC/OsuSchemeLinkIPCChannel.cs b/osu.Game/IPC/OsuSchemeLinkIPCChannel.cs new file mode 100644 index 0000000000..33318e329c --- /dev/null +++ b/osu.Game/IPC/OsuSchemeLinkIPCChannel.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Platform; +using osu.Game.Online; + +namespace osu.Game.IPC +{ + public class OsuSchemeLinkIPCChannel : IpcChannel + { + private readonly ILinkHandler? linkHandler; + + public OsuSchemeLinkIPCChannel(IIpcHost host, ILinkHandler? linkHandler = null) + : base(host) + { + this.linkHandler = linkHandler; + + MessageReceived += msg => + { + Debug.Assert(linkHandler != null); + linkHandler.HandleLink(msg.Link); + return null; + }; + } + + public async Task HandleLinkAsync(string url) + { + if (linkHandler == null) + { + await SendMessageAsync(new OsuSchemeLinkMessage(url)).ConfigureAwait(false); + return; + } + + linkHandler.HandleLink(url); + } + } + + public class OsuSchemeLinkMessage + { + public string Link { get; } + + public OsuSchemeLinkMessage(string link) + { + Link = link; + } + } +} diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 974abc4036..14a041b459 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings protected override void LoadComplete() { - realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, changes, error) => + realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) => { // The first fire of this is a bit redundant as this is being called in base.LoadComplete, // but this is safest in case the subscription is restored after a context recycle. diff --git a/osu.Game/Input/IdleTracker.cs b/osu.Game/Input/IdleTracker.cs index 279cfb5a4e..45036b3e41 100644 --- a/osu.Game/Input/IdleTracker.cs +++ b/osu.Game/Input/IdleTracker.cs @@ -80,11 +80,11 @@ namespace osu.Game.Input switch (e) { - case KeyDownEvent _: - case KeyUpEvent _: - case MouseDownEvent _: - case MouseUpEvent _: - case MouseMoveEvent _: + case KeyDownEvent: + case KeyUpEvent: + case MouseDownEvent: + case MouseUpEvent: + case MouseMoveEvent: return updateLastInteractionTime(); default: diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index 0dc95da2f4..0f0f560df9 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs b/osu.Game/Localisation/BeatmapOffsetControlStrings.cs index 417cf335e0..632a1ad0ea 100644 --- a/osu.Game/Localisation/BeatmapOffsetControlStrings.cs +++ b/osu.Game/Localisation/BeatmapOffsetControlStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/BindingSettingsStrings.cs b/osu.Game/Localisation/BindingSettingsStrings.cs index 39b5ac0d21..ad4a650a1f 100644 --- a/osu.Game/Localisation/BindingSettingsStrings.cs +++ b/osu.Game/Localisation/BindingSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/ButtonSystemStrings.cs b/osu.Game/Localisation/ButtonSystemStrings.cs index c71a99711b..ba4abf63a6 100644 --- a/osu.Game/Localisation/ButtonSystemStrings.cs +++ b/osu.Game/Localisation/ButtonSystemStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs index b07ed18f7b..7bd284a94e 100644 --- a/osu.Game/Localisation/ChatStrings.cs +++ b/osu.Game/Localisation/ChatStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 39dc7cf518..1ee562e122 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/DebugLocalisationStore.cs b/osu.Game/Localisation/DebugLocalisationStore.cs index 83ae4581a8..2b114b1bd8 100644 --- a/osu.Game/Localisation/DebugLocalisationStore.cs +++ b/osu.Game/Localisation/DebugLocalisationStore.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.Globalization; diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs index e6de4ddee9..74b2c8d892 100644 --- a/osu.Game/Localisation/DebugSettingsStrings.cs +++ b/osu.Game/Localisation/DebugSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs index 4bcbdcff7b..952ca22678 100644 --- a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs +++ b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs index 38f5860cc5..deac7d8628 100644 --- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs index 331a8c6764..3a7fe4bb12 100644 --- a/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupBeatmapScreenStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs b/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs index 0c73d7a85b..91b427e2ca 100644 --- a/osu.Game/Localisation/FirstRunSetupOverlayStrings.cs +++ b/osu.Game/Localisation/FirstRunSetupOverlayStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 1719b7d6e8..8a0f773551 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 8cf26a930d..2aa91f5245 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index d45fba6f17..82d03dbb5b 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index a73d67067e..38355d9041 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/InputSettingsStrings.cs b/osu.Game/Localisation/InputSettingsStrings.cs index a16dcd998a..e46b4cecf3 100644 --- a/osu.Game/Localisation/InputSettingsStrings.cs +++ b/osu.Game/Localisation/InputSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/JoystickSettingsStrings.cs b/osu.Game/Localisation/JoystickSettingsStrings.cs index efda1afd48..976ec1adde 100644 --- a/osu.Game/Localisation/JoystickSettingsStrings.cs +++ b/osu.Game/Localisation/JoystickSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs index f9094b9540..c13a1a10cb 100644 --- a/osu.Game/Localisation/Language.cs +++ b/osu.Game/Localisation/Language.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.ComponentModel; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs index 1a0b015050..b4326b8e39 100644 --- a/osu.Game/Localisation/LayoutSettingsStrings.cs +++ b/osu.Game/Localisation/LayoutSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/LeaderboardStrings.cs b/osu.Game/Localisation/LeaderboardStrings.cs index 14bc5b7af4..8e53f8e88c 100644 --- a/osu.Game/Localisation/LeaderboardStrings.cs +++ b/osu.Game/Localisation/LeaderboardStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs index 4cb514feb0..7a04bcd1ca 100644 --- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs +++ b/osu.Game/Localisation/MaintenanceSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index 3d99075922..e9af7147e3 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 563d6f7637..fd7225ad2e 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs index 956d5195e7..92cedce3e0 100644 --- a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs +++ b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/NamedOverlayComponentStrings.cs b/osu.Game/Localisation/NamedOverlayComponentStrings.cs index f6a50460dc..475bea2a4a 100644 --- a/osu.Game/Localisation/NamedOverlayComponentStrings.cs +++ b/osu.Game/Localisation/NamedOverlayComponentStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index c60e5d3415..382e0d81f4 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/NowPlayingStrings.cs b/osu.Game/Localisation/NowPlayingStrings.cs index 75d9b28ea6..f334637338 100644 --- a/osu.Game/Localisation/NowPlayingStrings.cs +++ b/osu.Game/Localisation/NowPlayingStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/OnlineSettingsStrings.cs b/osu.Game/Localisation/OnlineSettingsStrings.cs index ebb21afeb7..6862f4ac2c 100644 --- a/osu.Game/Localisation/OnlineSettingsStrings.cs +++ b/osu.Game/Localisation/OnlineSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 08901a641e..a356c9e20b 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/SettingsStrings.cs b/osu.Game/Localisation/SettingsStrings.cs index 5dcab39e10..aa2e2740eb 100644 --- a/osu.Game/Localisation/SettingsStrings.cs +++ b/osu.Game/Localisation/SettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index 42b72fdbb8..81035c5a5e 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs index 8d2bc21652..d62d348df9 100644 --- a/osu.Game/Localisation/TabletSettingsStrings.cs +++ b/osu.Game/Localisation/TabletSettingsStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/ToastStrings.cs b/osu.Game/Localisation/ToastStrings.cs index 00a0031293..52e75425bf 100644 --- a/osu.Game/Localisation/ToastStrings.cs +++ b/osu.Game/Localisation/ToastStrings.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.Localisation; namespace osu.Game.Localisation diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index 4e7af99ce9..a090b8c14c 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.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.Localisation; namespace osu.Game.Localisation @@ -106,6 +104,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RandomSelectionAlgorithm => new TranslatableString(getKey(@"random_selection_algorithm"), @"Random selection algorithm"); + /// + /// "Mod select hotkey style" + /// + public static LocalisableString ModSelectHotkeyStyle => new TranslatableString(getKey(@"mod_select_hotkey_style"), @"Mod select hotkey style"); + /// /// "no limit" /// diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs index 7104245d9e..c751530bf4 100644 --- a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs +++ b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs index f1c7c94638..08ab64fd08 100644 --- a/osu.Game/Migrations/20171019041408_InitialCreate.cs +++ b/osu.Game/Migrations/20171019041408_InitialCreate.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs index b464c15ce2..4cd234f2ef 100644 --- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs index 1d0164a1d9..4ec3952941 100644 --- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs index 53e8b887d8..006acf12cd 100644 --- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs index 765a6a5b58..6aba12f86f 100644 --- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs index 4da20141bc..fc2496bc24 100644 --- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs index 3379efa68e..5688455f79 100644 --- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180125143340_Settings.Designer.cs b/osu.Game/Migrations/20180125143340_Settings.Designer.cs index 68054d6404..4bb599eec1 100644 --- a/osu.Game/Migrations/20180125143340_Settings.Designer.cs +++ b/osu.Game/Migrations/20180125143340_Settings.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs index 8f7d0a6ed3..1feb37531f 100644 --- a/osu.Game/Migrations/20180125143340_Settings.cs +++ b/osu.Game/Migrations/20180125143340_Settings.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs index 3e97e78e61..8646d1d76b 100644 --- a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs +++ b/osu.Game/Migrations/20180131154205_AddMuteBinding.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 Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Infrastructure; using osu.Game.Database; diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs index 7d95a702b8..cdc4ef2e66 100644 --- a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs +++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs index df9d267a14..319748bed6 100644 --- a/osu.Game/Migrations/20180219060912_AddSkins.cs +++ b/osu.Game/Migrations/20180219060912_AddSkins.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs index 7e490c7833..f28408bfb3 100644 --- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs @@ -1,7 +1,5 @@ // using Microsoft.EntityFrameworkCore; - -#nullable disable using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs index 62c341b0c6..91eabe8868 100644 --- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs index a103cdad20..aaa11e88b6 100644 --- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs index b08fa7a899..d888ccd5a2 100644 --- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs index 5b1734554a..7eeacd56d7 100644 --- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs index 4ba0a4d0e6..fdea636ac6 100644 --- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs index ceef5b6948..5ab43da046 100644 --- a/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs +++ b/osu.Game/Migrations/20180913080842_AddRankStatus.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.cs index ec82925b03..bb147dff84 100644 --- a/osu.Game/Migrations/20180913080842_AddRankStatus.cs +++ b/osu.Game/Migrations/20180913080842_AddRankStatus.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs index d1185ef186..b387a45ecf 100644 --- a/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs +++ b/osu.Game/Migrations/20181007180454_StandardizePaths.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.cs index f9323c0b9b..30f27043a0 100644 --- a/osu.Game/Migrations/20181007180454_StandardizePaths.cs +++ b/osu.Game/Migrations/20181007180454_StandardizePaths.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs index 0797090c7b..120674671a 100644 --- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs +++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs index 43689f2a3c..ee825a1e9c 100644 --- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs +++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs index 3b70ad8d45..eee53182ce 100644 --- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs +++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs index f52edacc7f..58980132f3 100644 --- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs +++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.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 Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs index 51b63bd9eb..8e1e3a59f3 100644 --- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs +++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs index 00d807d5c4..f2eef600dc 100644 --- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs +++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs index 9d6515e5c6..348c42adb9 100644 --- a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs +++ b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs index 463d4fe1ad..7779b55bb7 100644 --- a/osu.Game/Migrations/20190525060824_SkinSettings.cs +++ b/osu.Game/Migrations/20190525060824_SkinSettings.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs index 1bc2e76ae5..9477369aa0 100644 --- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs index 61925a4cb4..0620a0624f 100644 --- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.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 Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs index c51b8739bc..c5fcc16f84 100644 --- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs +++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs index 19a2464157..f8ce354aa1 100644 --- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs +++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs index 95583e7587..826233a2b0 100644 --- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs index c1208e0bf1..af82b4db20 100644 --- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs index 3c7de0602b..22316b0380 100644 --- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs index 73cfb1725b..3d2ddbf6fc 100644 --- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs index 24acc3195f..1c05de832e 100644 --- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs index eaca8785f9..58a35a7bf3 100644 --- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs index 704af354c9..2c100d39b9 100644 --- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs index 7772c0e86b..4d3941dd20 100644 --- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs index 097bd6a244..b808c648da 100644 --- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs index 1cc9ab6120..887635fa85 100644 --- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs index ca9502422b..89bab3a0fa 100644 --- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs +++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs index 82c5b27769..7b579e27b9 100644 --- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs +++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs index 2a4e370649..afeb42130d 100644 --- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs +++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs index dd6fa23387..d1b09e2c1d 100644 --- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs +++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs index 9e09f2c69e..6e53d7fae0 100644 --- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs +++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.Designer.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs index 0598a41f09..f6fc1f4420 100644 --- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs +++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs b/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs index 0f7a2a5702..6d53c019ec 100644 --- a/osu.Game/Migrations/20211020081609_ResetSkinHashes.cs +++ b/osu.Game/Migrations/20211020081609_ResetSkinHashes.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 Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using osu.Game.Database; diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 7e38889fa6..036c26cb0a 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -1,7 +1,5 @@ // using System; - -#nullable disable using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 245760a00a..07d544260e 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -15,10 +15,12 @@ namespace osu.Game.Online.API { public class DummyAPIAccess : Component, IAPIProvider { + public const int DUMMY_USER_ID = 1001; + public Bindable LocalUser { get; } = new Bindable(new APIUser { Username = @"Dummy", - Id = 1001, + Id = DUMMY_USER_ID, }); public BindableList Friends { get; } = new BindableList(); diff --git a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs index 75b0cf8027..2d8a8b3b61 100644 --- a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs @@ -26,7 +26,7 @@ namespace osu.Game.Online.API.Requests var req = base.CreateWebRequest(); req.AddParameter("spotlight", spotlight.ToString()); - req.AddParameter("filter", sort.ToString().ToLower()); + req.AddParameter("filter", sort.ToString().ToLowerInvariant()); return req; } diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index ab45e8e48f..7dcf75950e 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -45,7 +45,7 @@ namespace osu.Game.Online.API.Requests Ruleset = ruleset; } - protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}"; + protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLowerInvariant()}" : $@"me/{Ruleset?.ShortName}"; private enum LookupType { diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 258708de2b..735fde333d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -69,6 +69,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"count_sliders")] public int SliderCount { get; set; } + [JsonProperty(@"count_spinners")] + public int SpinnerCount { get; set; } + [JsonProperty(@"version")] public string DifficultyName { get; set; } = string.Empty; diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs index 1b56362aec..f236607761 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScore.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -88,7 +88,7 @@ namespace osu.Game.Online.API.Requests.Responses /// public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null) { - var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException(); + var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally"); var rulesetInstance = ruleset.CreateInstance(); diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 1e3add6201..19708cc07d 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 4c762d002d..ec84b0643d 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -133,12 +133,14 @@ namespace osu.Game.Online.Chat ?? JoinChannel(new Channel(user)); } - private void currentChannelChanged(ValueChangedEvent e) + private void currentChannelChanged(ValueChangedEvent channel) { - bool isSelectorChannel = e.NewValue is ChannelListing.ChannelListingChannel; + bool isSelectorChannel = channel.NewValue is ChannelListing.ChannelListingChannel; if (!isSelectorChannel) - JoinChannel(e.NewValue); + JoinChannel(channel.NewValue); + + Logger.Log($"Current channel changed to {channel.NewValue}"); } /// @@ -447,9 +449,17 @@ namespace osu.Game.Online.Chat return channel; case ChannelType.PM: + Logger.Log($"Attempting to join PM channel {channel}"); + var createRequest = new CreateChannelRequest(channel); + createRequest.Failure += e => + { + Logger.Log($"Failed to join PM channel {channel} ({e.Message})"); + }; createRequest.Success += resChannel => { + Logger.Log($"Joined PM channel {channel} ({resChannel.ChannelID})"); + if (resChannel.ChannelID.HasValue) { channel.Id = resChannel.ChannelID.Value; @@ -463,9 +473,19 @@ namespace osu.Game.Online.Chat break; default: + Logger.Log($"Attempting to join public channel {channel}"); + var req = new JoinChannelRequest(channel); - req.Success += () => joinChannel(channel, fetchInitialMessages); - req.Failure += ex => LeaveChannel(channel); + req.Success += () => + { + Logger.Log($"Joined public channel {channel}"); + joinChannel(channel, fetchInitialMessages); + }; + req.Failure += e => + { + Logger.Log($"Failed to join public channel {channel} ({e.Message})"); + LeaveChannel(channel); + }; api.Queue(req); return channel; } diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index f9000c01ef..61e9eaa8c0 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -64,7 +64,7 @@ namespace osu.Game.Online this.preferMessagePack = preferMessagePack; apiState.BindTo(api.State); - apiState.BindValueChanged(state => connectIfPossible(), true); + apiState.BindValueChanged(_ => connectIfPossible(), true); } public void Reconnect() diff --git a/osu.Game/Online/ILinkHandler.cs b/osu.Game/Online/ILinkHandler.cs new file mode 100644 index 0000000000..1b8fad4bd9 --- /dev/null +++ b/osu.Game/Online/ILinkHandler.cs @@ -0,0 +1,30 @@ +// 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.Game.Online.Chat; + +namespace osu.Game.Online +{ + /// + /// Handle an arbitrary URL. Displays via in-game overlays where possible. + /// Methods can be called from a non-thread-safe non-game-loaded state. + /// + [Cached] + public interface ILinkHandler + { + /// + /// Handle an arbitrary URL. Displays via in-game overlays where possible. + /// This can be called from a non-thread-safe non-game-loaded state. + /// + /// The URL to load. + void HandleLink(string url); + + /// + /// Handle a specific . + /// This can be called from a non-thread-safe non-game-loaded state. + /// + /// The link to load. + void HandleLink(LinkDetails link); + } +} diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index df8717fe6d..2d2d82821c 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = @"your personal best".ToUpper(), + Text = @"your personal best".ToUpperInvariant(), Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), }, scoreContainer = new Container diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 13f14bca5b..6f597e5b10 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -79,7 +79,7 @@ namespace osu.Game.Online.Rooms TotalScore = TotalScore, MaxCombo = MaxCombo, BeatmapInfo = beatmap, - Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException(), + Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"), Statistics = Statistics, User = User, Accuracy = Accuracy, diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs index 032215f5d3..2fd8445980 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs @@ -101,7 +101,7 @@ namespace osu.Game.Online.Rooms // handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow). realmSubscription?.Dispose(); - realmSubscription = realm.RegisterForNotifications(r => filteredBeatmaps(), (items, changes, ___) => + realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) => { if (changes == null) return; diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index cab6bd0722..680bb16264 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) - && !s.DeletePending), (items, changes, ___) => + && !s.DeletePending), (items, _, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 4a43aa6c66..08e1e78d86 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -294,17 +294,21 @@ namespace osu.Game.Online.Spectator lastSend = tcs.Task; - SendFramesInternal(bundle).ContinueWith(t => Schedule(() => + SendFramesInternal(bundle).ContinueWith(t => { + // Handle exception outside of `Schedule` to ensure it doesn't go unovserved. bool wasSuccessful = t.Exception == null; - // If the last bundle send wasn't successful, try again without dequeuing. - if (wasSuccessful) - pendingFrameBundles.Dequeue(); + return Schedule(() => + { + // If the last bundle send wasn't successful, try again without dequeuing. + if (wasSuccessful) + pendingFrameBundles.Dequeue(); - tcs.SetResult(wasSuccessful); - sendNextBundleIfRequired(); - })); + tcs.SetResult(wasSuccessful); + sendNextBundleIfRequired(); + }); + }); } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3041761a6b..bd0a2680ae 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -24,6 +24,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; @@ -39,6 +40,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Localisation; +using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; @@ -69,7 +71,7 @@ namespace osu.Game /// The full osu! experience. Builds on top of to add menus and binding logic /// for initial components that are generally retrieved via DI. /// - public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager + public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). @@ -230,7 +232,7 @@ namespace osu.Game /// /// Unregisters a blocking that was not created by itself. /// - private void unregisterBlockingOverlay(OverlayContainer overlayContainer) + private void unregisterBlockingOverlay(OverlayContainer overlayContainer) => Schedule(() => { externalOverlays.Remove(overlayContainer); @@ -238,7 +240,7 @@ namespace osu.Game focusedOverlays.Remove(focusedOverlayContainer); overlayContainer.Expire(); - } + }); #endregion @@ -485,6 +487,7 @@ namespace osu.Game /// public void PresentBeatmap(IBeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { + Logger.Log($"Beginning {nameof(PresentBeatmap)} with beatmap {beatmap}"); Live databasedSet = null; if (beatmap.OnlineID > 0) @@ -521,6 +524,7 @@ namespace osu.Game } else { + Logger.Log($"Completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {selection.Ruleset}"); Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); } @@ -533,6 +537,8 @@ namespace osu.Game /// public void PresentScore(IScoreInfo score, ScorePresentType presentType = ScorePresentType.Results) { + Logger.Log($"Beginning {nameof(PresentScore)} with score {score}"); + // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. ScoreInfo databasedScoreInfo = null; @@ -567,6 +573,8 @@ namespace osu.Game PerformFromScreen(screen => { + Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score"); + Ruleset.Value = databasedScore.ScoreInfo.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); @@ -611,7 +619,6 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); - Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}"); } private void modsChanged(ValueChangedEvent> mods) @@ -637,6 +644,12 @@ namespace osu.Game Add(performFromMainMenuTask = new PerformFromMenuRunner(action, validScreens, () => ScreenStack.CurrentScreen)); } + public override void AttemptExit() + { + // Using PerformFromScreen gives the user a chance to interrupt the exit process if needed. + PerformFromScreen(menu => menu.Exit()); + } + /// /// Wait for the game (and target component) to become loaded and then run an action. /// @@ -674,27 +687,29 @@ namespace osu.Game { base.LoadComplete(); - foreach (var language in Enum.GetValues(typeof(Language)).OfType()) + var languages = Enum.GetValues(typeof(Language)).OfType(); + + var mappings = languages.Select(language => { #if DEBUG if (language == Language.debug) - { - Localisation.AddLanguage(Language.debug.ToString(), new DebugLocalisationStore()); - continue; - } + return new LocaleMapping("debug", new DebugLocalisationStore()); #endif string cultureCode = language.ToCultureCode(); try { - Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode)); + return new LocaleMapping(new ResourceManagerLocalisationStore(cultureCode)); } catch (Exception ex) { Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\""); + return null; } - } + }).Where(m => m != null); + + Localisation.AddLocaleMappings(mappings); // The next time this is updated is in UpdateAfterChildren, which occurs too late and results // in the cursor being shown for a few frames during the intro. @@ -705,13 +720,13 @@ namespace osu.Game SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value); + BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); BeatmapDownloader.PostNotification = n => Notifications.Post(n); ScoreDownloader.PostNotification = n => Notifications.Post(n); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PostImport = items => PresentScore(items.First().Value); + ScoreManager.PresentImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9908697bf1..57369419d2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -21,6 +21,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Midi; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; @@ -42,6 +44,8 @@ using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -85,7 +89,7 @@ namespace osu.Game public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; internal EndpointConfiguration CreateEndpoints() => - UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); @@ -240,7 +244,7 @@ namespace osu.Game dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); - largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore())); + largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore())); dependencies.Cache(largeStore); dependencies.CacheAs(this); @@ -264,14 +268,16 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); + dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, () => difficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API)); dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API)); - dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + // Add after all the above cache operations as it depends on them. AddInternal(difficultyCache); dependencies.Cache(userCache = new UserLookupCache()); @@ -417,14 +423,15 @@ namespace osu.Game /// /// Use to programatically exit the game as if the user was triggering via alt-f4. - /// Will keep persisting until an exit occurs (exit may be blocked multiple times). + /// By default, will keep persisting until an exit occurs (exit may be blocked multiple times). + /// May be interrupted (see 's override). /// - public void GracefullyExit() + public virtual void AttemptExit() { if (!OnExiting()) Exit(); else - Scheduler.AddDelayed(GracefullyExit, 2000); + Scheduler.AddDelayed(AttemptExit, 2000); } public bool Migrate(string path) @@ -444,7 +451,8 @@ namespace osu.Game readyToRun.Set(); }, false); - readyToRun.Wait(); + if (!readyToRun.Wait(30000)) + throw new TimeoutException("Attempting to block for migration took too long."); bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); @@ -465,10 +473,29 @@ namespace osu.Game protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); - private void onBeatmapChanged(ValueChangedEvent valueChangedEvent) + /// + /// Creates an input settings subsection for an . + /// + /// Should be overriden per-platform to provide settings for platform-specific handlers. + public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler) + { + switch (handler) + { + case MidiHandler: + return new InputSection.HandlerSection(handler); + + // return null for handlers that shouldn't have settings. + default: + return null; + } + } + + private void onBeatmapChanged(ValueChangedEvent beatmap) { if (IsLoaded && !ThreadSafety.IsUpdateThread) throw new InvalidOperationException("Global beatmap bindable must be changed from update thread."); + + Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}"); } private void onRulesetChanged(ValueChangedEvent r) @@ -558,6 +585,6 @@ namespace osu.Game ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null; } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 4457705676..767b8646e3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -161,13 +161,13 @@ namespace osu.Game.Overlays.BeatmapListing queueUpdateSearch(true); }); - searchControl.General.CollectionChanged += (_, __) => queueUpdateSearch(); + searchControl.General.CollectionChanged += (_, _) => queueUpdateSearch(); searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch()); searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); - searchControl.Extra.CollectionChanged += (_, __) => queueUpdateSearch(); - searchControl.Ranks.CollectionChanged += (_, __) => queueUpdateSearch(); + searchControl.Extra.CollectionChanged += (_, _) => queueUpdateSearch(); + searchControl.Ranks.CollectionChanged += (_, _) => queueUpdateSearch(); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); searchControl.ExplicitContent.BindValueChanged(_ => queueUpdateSearch()); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 8d89bb3cc7..b27d9b3f1e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -156,7 +156,7 @@ namespace osu.Game.Overlays.BeatmapSet { base.LoadComplete(); - ruleset.ValueChanged += r => updateDisplay(); + ruleset.ValueChanged += _ => updateDisplay(); // done here so everything can bind in intialization and get the first trigger Beatmap.TriggerChange(); @@ -274,8 +274,10 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0.5f } }, - icon = new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) + icon = new DifficultyIcon(beatmapInfo) { + ShowTooltip = false, + Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) }, Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(size - tile_icon_padding * 2), diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 2ea94bb412..c2e54d0d7b 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores scope.BindValueChanged(_ => getScores()); ruleset.BindValueChanged(_ => getScores()); - modSelector.SelectedMods.CollectionChanged += (_, __) => getScores(); + modSelector.SelectedMods.CollectionChanged += (_, _) => getScores(); Beatmap.BindValueChanged(onBeatmapChanged); user.BindValueChanged(onUserChanged, true); diff --git a/osu.Game/Overlays/Chat/DaySeparator.cs b/osu.Game/Overlays/Chat/DaySeparator.cs index c7961d1a39..be0b53785c 100644 --- a/osu.Game/Overlays/Chat/DaySeparator.cs +++ b/osu.Game/Overlays/Chat/DaySeparator.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -79,7 +80,7 @@ namespace osu.Game.Overlays.Chat { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(), + Text = time.ToLocalTime().ToLocalisableString(@"dd MMMM yyyy").ToUpper(), Font = OsuFont.Torus.With(size: TextSize, weight: FontWeight.SemiBold), Colour = colourProvider?.Content1 ?? Colour4.White, }, diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 81a408598b..c491c0c695 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -18,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.ChannelList; @@ -100,23 +101,10 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Height = top_bar_height, }, - channelList = new ChannelList - { - RelativeSizeAxes = Axes.Y, - Width = side_bar_width, - Padding = new MarginPadding { Top = top_bar_height }, - }, new Container { RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding - { - Top = top_bar_height, - Left = side_bar_width, - Bottom = chat_bar_height, - }, + Padding = new MarginPadding { Top = top_bar_height }, Children = new Drawable[] { new Box @@ -124,24 +112,50 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background4, }, - currentChannelContainer = new Container + new OnlineViewContainer("Sign in to chat") { RelativeSizeAxes = Axes.Both, - }, - loading = new LoadingLayer(true), - channelListing = new ChannelListing - { - RelativeSizeAxes = Axes.Both, - }, - }, - }, - textBar = new ChatTextBar - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Padding = new MarginPadding { Left = side_bar_width }, - }, + Children = new Drawable[] + { + channelList = new ChannelList + { + RelativeSizeAxes = Axes.Y, + Width = side_bar_width, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding + { + Left = side_bar_width, + Bottom = chat_bar_height, + }, + Children = new Drawable[] + { + currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(true), + channelListing = new ChannelListing + { + RelativeSizeAxes = Axes.Both, + }, + }, + }, + textBar = new ChatTextBar + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Padding = new MarginPadding { Left = side_bar_width }, + }, + } + } + } + } }; } diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 2e8080ba43..e2a7e78356 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Comments } }); - textBox.OnCommit += (u, v) => + textBox.OnCommit += (_, _) => { if (commitButton.IsBlocked.Value) return; diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 4ab6f4f88d..4a836e0e62 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -251,8 +251,8 @@ namespace osu.Game.Overlays.Comments if (bundle.HasMore) { int loadedTopLevelComments = 0; - pinnedContent.Children.OfType().ForEach(p => loadedTopLevelComments++); - content.Children.OfType().ForEach(p => loadedTopLevelComments++); + pinnedContent.Children.OfType().ForEach(_ => loadedTopLevelComments++); + content.Children.OfType().ForEach(_ => loadedTopLevelComments++); moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments; moreButton.IsLoading = false; diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index bfeaf50b53..bfd356193d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Dashboard.Friends controlBackground.Colour = colourProvider.Background5; apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, __) => Schedule(() => Users = apiFriends.ToList()), true); + apiFriends.BindCollectionChanged((_, _) => Schedule(() => Users = apiFriends.ToList()), true); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index 08522b2921..f545e2892f 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Login } }; - password.OnCommit += (sender, newText) => performLogin(); + password.OnCommit += (_, _) => performLogin(); if (api?.LastLoginError?.Message is string error) errorText.AddErrors(new[] { error }); diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 53e8452dd1..1c007c913e 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.MedalSplash s.Font = s.Font.With(size: 16); }); - medalContainer.OnLoadComplete += d => + medalContainer.OnLoadComplete += _ => { unlocked.Position = new Vector2(0f, medalContainer.DrawSize.Y / 2 + 10); infoFlow.Position = new Vector2(0f, unlocked.Position.Y + 90); diff --git a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs index a6f412c00a..8cedd6b374 100644 --- a/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs +++ b/osu.Game/Overlays/Mods/IncompatibilityDisplayingModPanel.cs @@ -46,8 +46,8 @@ namespace osu.Game.Overlays.Mods && !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(Mod)); } - protected override Colour4 BackgroundColour => incompatible.Value ? (Colour4)ColourProvider.Background6 : base.BackgroundColour; - protected override Colour4 ForegroundColour => incompatible.Value ? (Colour4)ColourProvider.Background5 : base.ForegroundColour; + protected override Colour4 BackgroundColour => incompatible.Value ? ColourProvider.Background6 : base.BackgroundColour; + protected override Colour4 ForegroundColour => incompatible.Value ? ColourProvider.Background5 : base.ForegroundColour; protected override void UpdateState() { diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs new file mode 100644 index 0000000000..4f3c18fc43 --- /dev/null +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -0,0 +1,98 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Mods; +using osuTK.Input; + +namespace osu.Game.Overlays.Mods.Input +{ + /// + /// Uses bindings from stable 1:1. + /// + public class ClassicModHotkeyHandler : IModHotkeyHandler + { + private static readonly Dictionary mod_type_lookup = new Dictionary + { + [Key.Q] = new[] { typeof(ModEasy) }, + [Key.W] = new[] { typeof(ModNoFail) }, + [Key.E] = new[] { typeof(ModHalfTime) }, + [Key.A] = new[] { typeof(ModHardRock) }, + [Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) }, + [Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) }, + [Key.F] = new[] { typeof(ModHidden) }, + [Key.G] = new[] { typeof(ModFlashlight) }, + [Key.Z] = new[] { typeof(ModRelax) }, + [Key.V] = new[] { typeof(ModAutoplay), typeof(ModCinema) } + }; + + private readonly bool allowIncompatibleSelection; + + public ClassicModHotkeyHandler(bool allowIncompatibleSelection) + { + this.allowIncompatibleSelection = allowIncompatibleSelection; + } + + public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods) + { + if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) + return false; + + var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray(); + + if (matchingMods.Length == 0) + return false; + + if (matchingMods.Length == 1) + { + matchingMods.Single().Active.Toggle(); + return true; + } + + if (allowIncompatibleSelection) + { + // easier path - multiple incompatible mods can be active at a time. + // this is used in the free mod select overlay. + // in this case, just toggle everything. + bool anyActive = matchingMods.Any(mod => mod.Active.Value); + foreach (var mod in matchingMods) + mod.Active.Value = !anyActive; + return true; + } + + // we now know there are multiple possible mods to handle, and only one of them can be active at a time. + // let's make sure of this just in case. + Debug.Assert(matchingMods.Count(modState => modState.Active.Value) <= 1); + int currentSelectedIndex = Array.FindIndex(matchingMods, modState => modState.Active.Value); + + // `FindIndex` will return -1 if it doesn't find the item. + // this is convenient in the forward direction, since if we add 1 then we'll end up at the first item, + // but less so in the backwards direction. + // for convenience, detect this situation and set the index to one index past the last item. + // this makes it so that if we subtract 1 then we'll end up at the last item again. + if (currentSelectedIndex < 0 && e.ShiftPressed) + currentSelectedIndex = matchingMods.Length; + + int indexToSelect = e.ShiftPressed ? currentSelectedIndex - 1 : currentSelectedIndex + 1; + + // `currentSelectedIndex` and `indexToSelect` can both be equal to -1 or `matchingMods.Length`. + // if the former is beyond array range, it means nothing was previously selected and so there's nothing to deselect. + // if the latter is beyond array range, it means that either the previous selection was first and we're going backwards, + // or it was last and we're going forwards. + // in either case there is nothing to select. + if (currentSelectedIndex >= 0 && currentSelectedIndex <= matchingMods.Length - 1) + matchingMods[currentSelectedIndex].Active.Value = false; + if (indexToSelect >= 0 && indexToSelect <= matchingMods.Length - 1) + matchingMods[indexToSelect].Active.Value = true; + + return true; + } + + private static bool matches(ModState modState, Type[] typesToMatch) + => typesToMatch.Any(typeToMatch => typeToMatch.IsInstanceOfType(modState.Mod)); + } +} diff --git a/osu.Game/Overlays/Mods/Input/IModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/IModHotkeyHandler.cs new file mode 100644 index 0000000000..d2cc0e84d2 --- /dev/null +++ b/osu.Game/Overlays/Mods/Input/IModHotkeyHandler.cs @@ -0,0 +1,22 @@ +// 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 osu.Framework.Input.Events; + +namespace osu.Game.Overlays.Mods.Input +{ + /// + /// Encapsulates strategies of handling mod hotkeys on the . + /// + public interface IModHotkeyHandler + { + /// + /// Attempt to handle the supplied as a selection of one of the mods in . + /// + /// The event representing the user's keypress. + /// The list of currently available mods. + /// Whether the supplied event was handled as a mod selection/deselection. + bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods); + } +} diff --git a/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs b/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs new file mode 100644 index 0000000000..6375b37f8b --- /dev/null +++ b/osu.Game/Overlays/Mods/Input/ModSelectHotkeyStyle.cs @@ -0,0 +1,27 @@ +// 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.Rulesets.Mods; + +namespace osu.Game.Overlays.Mods.Input +{ + /// + /// The style of hotkey handling to use on the mod select screen. + /// + public enum ModSelectHotkeyStyle + { + /// + /// Each letter row on the keyboard controls one of the three first s. + /// Individual letters in a row trigger the mods in a sequential fashion. + /// Uses . + /// + Sequential, + + /// + /// Matches keybindings from stable 1:1. + /// 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. + /// + Classic + } +} diff --git a/osu.Game/Overlays/Mods/Input/NoopModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/NoopModHotkeyHandler.cs new file mode 100644 index 0000000000..3f7a6298a1 --- /dev/null +++ b/osu.Game/Overlays/Mods/Input/NoopModHotkeyHandler.cs @@ -0,0 +1,17 @@ +// 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 osu.Framework.Input.Events; + +namespace osu.Game.Overlays.Mods.Input +{ + /// + /// A no-op implementation of . + /// Used when a column is not handling any hotkeys at all. + /// + public class NoopModHotkeyHandler : IModHotkeyHandler + { + public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods) => false; + } +} diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs new file mode 100644 index 0000000000..dedb556304 --- /dev/null +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -0,0 +1,59 @@ +// 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.Input.Events; +using osu.Game.Rulesets.Mods; +using osuTK.Input; + +namespace osu.Game.Overlays.Mods.Input +{ + /// + /// This implementation of receives a sequence of s, + /// and maps the sequence of keys onto the items it is provided in . + /// In this case, particular mods are not bound to particular keys, the hotkeys are a byproduct of mod ordering. + /// + public class SequentialModHotkeyHandler : IModHotkeyHandler + { + public static SequentialModHotkeyHandler Create(ModType modType) + { + switch (modType) + { + case ModType.DifficultyReduction: + return new SequentialModHotkeyHandler(new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }); + + case ModType.DifficultyIncrease: + return new SequentialModHotkeyHandler(new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }); + + case ModType.Automation: + return new SequentialModHotkeyHandler(new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }); + + default: + throw new ArgumentOutOfRangeException(nameof(modType), modType, $"Cannot create {nameof(SequentialModHotkeyHandler)} for provided mod type"); + } + } + + private readonly Key[] toggleKeys; + + private SequentialModHotkeyHandler(Key[] keys) + { + toggleKeys = keys; + } + + public bool HandleHotkeyPressed(KeyDownEvent e, IEnumerable availableMods) + { + int index = Array.IndexOf(toggleKeys, e.Key); + if (index < 0) + return false; + + var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); + if (modState == null) + return false; + + modState.Active.Toggle(); + return true; + } + } +} diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 563e9a8d55..3f788d10e3 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -17,14 +17,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Overlays.Mods.Input; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; -using osuTK.Input; namespace osu.Game.Overlays.Mods { @@ -70,7 +71,7 @@ namespace osu.Game.Overlays.Mods protected virtual ModPanel CreateModPanel(ModState mod) => new ModPanel(mod); - private readonly Key[]? toggleKeys; + private readonly bool allowIncompatibleSelection; private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -81,15 +82,18 @@ namespace osu.Game.Overlays.Mods private Colour4 accentColour; + private Bindable hotkeyStyle = null!; + private IModHotkeyHandler hotkeyHandler = null!; + private Task? latestLoadTask; internal bool ItemsLoaded => latestLoadTask == null; private const float header_height = 42; - public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = null) + public ModColumn(ModType modType, bool allowIncompatibleSelection) { ModType = modType; - this.toggleKeys = toggleKeys; + this.allowIncompatibleSelection = allowIncompatibleSelection; Width = 320; RelativeSizeAxes = Axes.Y; @@ -197,7 +201,7 @@ namespace osu.Game.Overlays.Mods createHeaderText(); - if (allowBulkSelection) + if (allowIncompatibleSelection) { controlContainer.Height = 35; controlContainer.Add(toggleAllCheckbox = new ToggleAllCheckbox(this) @@ -231,7 +235,7 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours, OsuConfigManager configManager) { headerBackground.Colour = accentColour = colours.ForModType(ModType); @@ -243,6 +247,8 @@ namespace osu.Game.Overlays.Mods contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background3); contentBackground.Colour = colourProvider.Background4; + + hotkeyStyle = configManager.GetBindable(OsuSetting.ModSelectHotkeyStyle); } protected override void LoadComplete() @@ -250,6 +256,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true); + hotkeyStyle.BindValueChanged(val => hotkeyHandler = createHotkeyHandler(val.NewValue), true); asyncLoadPanels(); } @@ -423,19 +430,32 @@ namespace osu.Game.Overlays.Mods #region Keyboard selection support + /// + /// Creates an appropriate for this column's and + /// the supplied . + /// + private IModHotkeyHandler createHotkeyHandler(ModSelectHotkeyStyle hotkeyStyle) + { + switch (ModType) + { + case ModType.DifficultyReduction: + case ModType.DifficultyIncrease: + case ModType.Automation: + return hotkeyStyle == ModSelectHotkeyStyle.Sequential + ? SequentialModHotkeyHandler.Create(ModType) + : new ClassicModHotkeyHandler(allowIncompatibleSelection); + + default: + return new NoopModHotkeyHandler(); + } + } + protected override bool OnKeyDown(KeyDownEvent e) { - if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false; - if (toggleKeys == null) return false; + if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.Repeat) + return false; - int index = Array.IndexOf(toggleKeys, e.Key); - if (index < 0) return false; - - var modState = availableMods.ElementAtOrDefault(index); - if (modState == null || modState.Filtered.Value) return false; - - modState.Active.Toggle(); - return true; + return hotkeyHandler.HandleHotkeyPressed(e, availableMods); } #endregion diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index 4d14a31189..02eb395bd9 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -216,9 +216,9 @@ namespace osu.Game.Overlays.Mods base.OnMouseUp(e); } - protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : (Colour4)ColourProvider.Background3; - protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : (Colour4)ColourProvider.Background2; - protected virtual Colour4 TextColour => Active.Value ? (Colour4)ColourProvider.Background6 : Colour4.White; + protected virtual Colour4 BackgroundColour => Active.Value ? activeColour.Darken(0.3f) : ColourProvider.Background3; + protected virtual Colour4 ForegroundColour => Active.Value ? activeColour : ColourProvider.Background2; + protected virtual Colour4 TextColour => Active.Value ? ColourProvider.Background6 : Colour4.White; protected virtual void UpdateState() { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ad02f079a5..69cb3a49fc 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -21,7 +21,6 @@ using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; -using osuTK.Input; namespace osu.Game.Overlays.Mods { @@ -41,7 +40,7 @@ namespace osu.Game.Overlays.Mods /// public Bindable>> AvailableMods { get; } = new Bindable>>(new Dictionary>()); - private Func isValidMod = m => true; + private Func isValidMod = _ => true; /// /// A function determining whether each mod in the column should be displayed. @@ -68,7 +67,7 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool AllowCustomisation => true; - protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys); + protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false); protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; @@ -160,9 +159,9 @@ namespace osu.Game.Overlays.Mods Padding = new MarginPadding { Bottom = 10 }, Children = new[] { - createModColumnContent(ModType.DifficultyReduction, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }), - createModColumnContent(ModType.DifficultyIncrease, new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }), - createModColumnContent(ModType.Automation, new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }), + createModColumnContent(ModType.DifficultyReduction), + createModColumnContent(ModType.DifficultyIncrease), + createModColumnContent(ModType.Automation), createModColumnContent(ModType.Conversion), createModColumnContent(ModType.Fun) } @@ -264,9 +263,9 @@ namespace osu.Game.Overlays.Mods column.DeselectAll(); } - private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) + private ColumnDimContainer createModColumnContent(ModType modType) { - var column = CreateModColumn(modType, toggleKeys).With(column => + var column = CreateModColumn(modType).With(column => { // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden. column.Margin = new MarginPadding { Right = 10 }; @@ -690,11 +689,11 @@ namespace osu.Game.Overlays.Mods switch (e) { - case ClickEvent _: + case ClickEvent: OnClicked?.Invoke(); return true; - case MouseEvent _: + case MouseEvent: return true; } diff --git a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs b/osu.Game/Overlays/Mods/UserModSelectOverlay.cs index b8f4b8a196..1090306c5b 100644 --- a/osu.Game/Overlays/Mods/UserModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/UserModSelectOverlay.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.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Game.Rulesets.Mods; using osu.Game.Utils; -using osuTK.Input; namespace osu.Game.Overlays.Mods { @@ -19,7 +15,7 @@ namespace osu.Game.Overlays.Mods { } - protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); + protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false); protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) { @@ -44,8 +40,8 @@ namespace osu.Game.Overlays.Mods private class UserModColumn : ModColumn { - public UserModColumn(ModType modType, bool allowBulkSelection, [CanBeNull] Key[] toggleKeys = null) - : base(modType, allowBulkSelection, toggleKeys) + public UserModColumn(ModType modType, bool allowIncompatibleSelection) + : base(modType, allowIncompatibleSelection) { } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 3c25fc0421..e33fc8064f 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Music }, }; - filter.Search.OnCommit += (sender, newText) => + filter.Search.OnCommit += (_, _) => { list.FirstVisibleSet?.PerformRead(set => { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index bce7e9b797..4a10f30a7a 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Database; @@ -106,10 +107,12 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return; + Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}"); NextTrack(); } else if (!IsPlaying) { + Logger.Log($"{nameof(MusicController)} starting playback to {nameof(EnsurePlayingSomething)}"); Play(); } } diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 69094a0df7..eb76522e11 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -146,7 +147,7 @@ namespace osu.Game.Overlays.News }, new OsuSpriteText { - Text = date.ToString("d MMM yyyy").ToUpper(), + Text = date.ToLocalisableString(@"d MMM yyyy").ToUpper(), Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), Margin = new MarginPadding { diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index d76df5b1ed..719e77db83 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Notifications case ProgressNotificationState.Completed: loadingSpinner.Hide(); NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); - this.FadeOut(200).Finally(d => Completed()); + this.FadeOut(200).Finally(_ => Completed()); break; } } diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index c66811a3e9..3bed5e7e4c 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.OSD if (val) selectedOption = 0; break; - case Enum _: + case Enum: var values = Enum.GetValues(description.RawValue.GetType()); optionCount = values.Length; selectedOption = Convert.ToInt32(description.RawValue); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 54a262f6a6..90a357a281 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Spacing = new Vector2(2), Children = Score.Mods.Select(mod => { - var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException(); + var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {Score.RulesetID} not found locally"); return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym)) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 2c8e809379..28642f12a1 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -160,13 +160,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); - windowModeDropdown.Current.BindValueChanged(mode => + windowModeDropdown.Current.BindValueChanged(_ => { updateDisplayModeDropdowns(); updateScreenModeWarning(); }, true); - windowModes.BindCollectionChanged((sender, args) => + windowModes.BindCollectionChanged((_, _) => { if (windowModes.Count > 1) windowModeDropdown.Show(); @@ -190,7 +190,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics updateDisplayModeDropdowns(); }), true); - scalingMode.BindValueChanged(mode => + scalingMode.BindValueChanged(_ => { scalingSettings.ClearTransforms(); scalingSettings.AutoSizeDuration = transition_duration; diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 303fb79d16..b815239c9f 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, true); } - private class SensitivitySetting : SettingsSlider + public class SensitivitySetting : SettingsSlider { public SensitivitySetting() { @@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - private class SensitivitySlider : OsuSliderBar + public class SensitivitySlider : OsuSliderBar { public override LocalisableString TooltipText => Current.Disabled ? MouseSettingsStrings.EnableHighPrecisionForSensitivityAdjust : $"{base.TooltipText}x"; } diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 9a4a2ddb57..4d75537f6b 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -7,10 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Handlers; -using osu.Framework.Input.Handlers.Joystick; -using osu.Framework.Input.Handlers.Midi; -using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Localisation; @@ -24,9 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections public override LocalisableString Header => InputSettingsStrings.InputSectionHeader; - [Resolved] - private GameHost host { get; set; } - public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.Keyboard @@ -38,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections } [BackgroundDependencyLoader] - private void load() + private void load(GameHost host, OsuGameBase game) { Children = new Drawable[] { @@ -47,45 +40,14 @@ namespace osu.Game.Overlays.Settings.Sections foreach (var handler in host.AvailableInputHandlers) { - var handlerSection = createSectionFor(handler); + var handlerSection = game.CreateSettingsSubsectionFor(handler); if (handlerSection != null) Add(handlerSection); } } - private SettingsSubsection createSectionFor(InputHandler handler) - { - SettingsSubsection section; - - switch (handler) - { - // ReSharper disable once SuspiciousTypeConversion.Global (net standard fuckery) - case ITabletHandler th: - section = new TabletSettings(th); - break; - - case MouseHandler mh: - section = new MouseSettings(mh); - break; - - // whitelist the handlers which should be displayed to avoid any weird cases of users touching settings they shouldn't. - case JoystickHandler jh: - section = new JoystickSettings(jh); - break; - - case MidiHandler _: - section = new HandlerSection(handler); - break; - - default: - return null; - } - - return section; - } - - private class HandlerSection : SettingsSubsection + public class HandlerSection : SettingsSubsection { private readonly InputHandler handler; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs new file mode 100644 index 0000000000..453dbd2e18 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class BeatmapSettings : SettingsSubsection + { + protected override LocalisableString Header => "Beatmaps"; + + private SettingsButton importBeatmapsButton = null!; + private SettingsButton deleteBeatmapsButton = null!; + private SettingsButton deleteBeatmapVideosButton = null!; + private SettingsButton restoreButton = null!; + private SettingsButton undeleteButton = null!; + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay) + { + if (legacyImportManager?.SupportsImportFromStable == true) + { + Add(importBeatmapsButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable, + Action = () => + { + importBeatmapsButton.Enabled.Value = false; + legacyImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(_ => Schedule(() => importBeatmapsButton.Enabled.Value = true)); + } + }); + } + + Add(deleteBeatmapsButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllBeatmaps, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => + { + deleteBeatmapsButton.Enabled.Value = false; + Task.Run(() => beatmaps.Delete()).ContinueWith(_ => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); + })); + } + }); + + Add(deleteBeatmapVideosButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos, + Action = () => + { + dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() => + { + deleteBeatmapVideosButton.Enabled.Value = false; + Task.Run(beatmaps.DeleteAllVideos).ContinueWith(_ => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true)); + })); + } + }); + AddRange(new Drawable[] + { + restoreButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.RestoreAllHiddenDifficulties, + Action = () => + { + restoreButton.Enabled.Value = false; + Task.Run(beatmaps.RestoreAll).ContinueWith(_ => Schedule(() => restoreButton.Enabled.Value = true)); + } + }, + undeleteButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.RestoreAllRecentlyDeletedBeatmaps, + Action = () => + { + undeleteButton.Enabled.Value = false; + Task.Run(beatmaps.UndeleteAll).ContinueWith(_ => Schedule(() => undeleteButton.Enabled.Value = true)); + } + } + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs new file mode 100644 index 0000000000..5367f644ca --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs @@ -0,0 +1,46 @@ +// 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.Localisation; +using osu.Game.Collections; +using osu.Game.Database; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class CollectionsSettings : SettingsSubsection + { + protected override LocalisableString Header => "Collections"; + + private SettingsButton importCollectionsButton = null!; + + [BackgroundDependencyLoader] + private void load(CollectionManager? collectionManager, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay) + { + if (collectionManager == null) return; + + if (legacyImportManager?.SupportsImportFromStable == true) + { + Add(importCollectionsButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.ImportCollectionsFromStable, + Action = () => + { + importCollectionsButton.Enabled.Value = false; + legacyImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(_ => Schedule(() => importCollectionsButton.Enabled.Value = true)); + } + }); + } + + Add(new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllCollections, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(collectionManager.DeleteAll)); + } + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs deleted file mode 100644 index 916c607c1c..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ /dev/null @@ -1,177 +0,0 @@ -// 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.Threading.Tasks; -using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Collections; -using osu.Game.Database; -using osu.Game.Localisation; -using osu.Game.Scoring; -using osu.Game.Skinning; - -namespace osu.Game.Overlays.Settings.Sections.Maintenance -{ - public class GeneralSettings : SettingsSubsection - { - protected override LocalisableString Header => "General"; - - private SettingsButton importBeatmapsButton; - private SettingsButton importScoresButton; - private SettingsButton importSkinsButton; - private SettingsButton importCollectionsButton; - private SettingsButton deleteBeatmapsButton; - private SettingsButton deleteScoresButton; - private SettingsButton deleteSkinsButton; - private SettingsButton restoreButton; - private SettingsButton undeleteButton; - private SettingsButton deleteBeatmapVideosButton; - - [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, IDialogOverlay dialogOverlay) - { - if (legacyImportManager?.SupportsImportFromStable == true) - { - Add(importBeatmapsButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable, - Action = () => - { - importBeatmapsButton.Enabled.Value = false; - legacyImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(t => Schedule(() => importBeatmapsButton.Enabled.Value = true)); - } - }); - } - - Add(deleteBeatmapsButton = new DangerousSettingsButton - { - Text = MaintenanceSettingsStrings.DeleteAllBeatmaps, - Action = () => - { - dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => - { - deleteBeatmapsButton.Enabled.Value = false; - Task.Run(() => beatmaps.Delete()).ContinueWith(t => Schedule(() => deleteBeatmapsButton.Enabled.Value = true)); - })); - } - }); - - Add(deleteBeatmapVideosButton = new DangerousSettingsButton - { - Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos, - Action = () => - { - dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() => - { - deleteBeatmapVideosButton.Enabled.Value = false; - Task.Run(beatmaps.DeleteAllVideos).ContinueWith(t => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true)); - })); - } - }); - - if (legacyImportManager?.SupportsImportFromStable == true) - { - Add(importScoresButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.ImportScoresFromStable, - Action = () => - { - importScoresButton.Enabled.Value = false; - legacyImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(t => Schedule(() => importScoresButton.Enabled.Value = true)); - } - }); - } - - Add(deleteScoresButton = new DangerousSettingsButton - { - Text = MaintenanceSettingsStrings.DeleteAllScores, - Action = () => - { - dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => - { - deleteScoresButton.Enabled.Value = false; - Task.Run(() => scores.Delete()).ContinueWith(t => Schedule(() => deleteScoresButton.Enabled.Value = true)); - })); - } - }); - - if (legacyImportManager?.SupportsImportFromStable == true) - { - Add(importSkinsButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.ImportSkinsFromStable, - Action = () => - { - importSkinsButton.Enabled.Value = false; - legacyImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(t => Schedule(() => importSkinsButton.Enabled.Value = true)); - } - }); - } - - Add(deleteSkinsButton = new DangerousSettingsButton - { - Text = MaintenanceSettingsStrings.DeleteAllSkins, - Action = () => - { - dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => - { - deleteSkinsButton.Enabled.Value = false; - Task.Run(() => skins.Delete()).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); - })); - } - }); - - if (collectionManager != null) - { - if (legacyImportManager?.SupportsImportFromStable == true) - { - Add(importCollectionsButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.ImportCollectionsFromStable, - Action = () => - { - importCollectionsButton.Enabled.Value = false; - legacyImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); - } - }); - } - - Add(new DangerousSettingsButton - { - Text = MaintenanceSettingsStrings.DeleteAllCollections, - Action = () => - { - dialogOverlay?.Push(new MassDeleteConfirmationDialog(collectionManager.DeleteAll)); - } - }); - } - - AddRange(new Drawable[] - { - restoreButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.RestoreAllHiddenDifficulties, - Action = () => - { - restoreButton.Enabled.Value = false; - Task.Run(beatmaps.RestoreAll).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true)); - } - }, - undeleteButton = new SettingsButton - { - Text = MaintenanceSettingsStrings.RestoreAllRecentlyDeletedBeatmaps, - Action = () => - { - undeleteButton.Enabled.Value = false; - Task.Run(beatmaps.UndeleteAll).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); - } - }, - }); - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 7e6341cd1b..0d32e33d87 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () => { (storage as OsuStorage)?.ChangeDataPath(target.FullName); - game.GracefullyExit(); + game.Exit(); }, () => { })); }, () => { })); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs new file mode 100644 index 0000000000..70e11d45dc --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Localisation; +using osu.Game.Scoring; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class ScoreSettings : SettingsSubsection + { + protected override LocalisableString Header => "Scores"; + + private SettingsButton importScoresButton = null!; + private SettingsButton deleteScoresButton = null!; + + [BackgroundDependencyLoader] + private void load(ScoreManager scores, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay) + { + if (legacyImportManager?.SupportsImportFromStable == true) + { + Add(importScoresButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.ImportScoresFromStable, + Action = () => + { + importScoresButton.Enabled.Value = false; + legacyImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(_ => Schedule(() => importScoresButton.Enabled.Value = true)); + } + }); + } + + Add(deleteScoresButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllScores, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => + { + deleteScoresButton.Enabled.Value = false; + Task.Run(() => scores.Delete()).ContinueWith(_ => Schedule(() => deleteScoresButton.Enabled.Value = true)); + })); + } + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs new file mode 100644 index 0000000000..c95b1d4dd8 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Localisation; +using osu.Game.Skinning; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class SkinSettings : SettingsSubsection + { + protected override LocalisableString Header => "Skins"; + + private SettingsButton importSkinsButton = null!; + private SettingsButton deleteSkinsButton = null!; + + [BackgroundDependencyLoader] + private void load(SkinManager skins, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay) + { + if (legacyImportManager?.SupportsImportFromStable == true) + { + Add(importSkinsButton = new SettingsButton + { + Text = MaintenanceSettingsStrings.ImportSkinsFromStable, + Action = () => + { + importSkinsButton.Enabled.Value = false; + legacyImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(_ => Schedule(() => importSkinsButton.Enabled.Value = true)); + } + }); + } + + Add(deleteSkinsButton = new DangerousSettingsButton + { + Text = MaintenanceSettingsStrings.DeleteAllSkins, + Action = () => + { + dialogOverlay?.Push(new MassDeleteConfirmationDialog(() => + { + deleteSkinsButton.Enabled.Value = false; + Task.Run(() => skins.Delete()).ContinueWith(_ => Schedule(() => deleteSkinsButton.Enabled.Value = true)); + })); + } + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index cfe1961bc1..841a7d0abe 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.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; @@ -24,7 +22,10 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - new GeneralSettings() + new BeatmapSettings(), + new SkinSettings(), + new CollectionsSettings(), + new ScoreSettings() }; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 6494b59b09..741b6b5815 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -82,12 +82,12 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Current = dropdownBindable; - realmSubscription = realm.RegisterForNotifications(r => realm.Realm.All() + realmSubscription = realm.RegisterForNotifications(_ => realm.Realm.All() .Where(s => !s.DeletePending) .OrderByDescending(s => s.Protected) // protected skins should be at the top. .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged); - configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig)); + configBindable.BindValueChanged(_ => Scheduler.AddOnce(updateSelectedSkinFromConfig)); dropdownBindable.BindValueChanged(dropdownSelectionChanged); } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index 8cec7bbb30..507e116723 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; +using osu.Game.Overlays.Mods.Input; namespace osu.Game.Overlays.Settings.Sections.UserInterface { @@ -61,6 +62,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface { LabelText = UserInterfaceStrings.RandomSelectionAlgorithm, Current = config.GetBindable(OsuSetting.RandomSelectAlgorithm), + }, + new SettingsEnumDropdown + { + LabelText = UserInterfaceStrings.ModSelectHotkeyStyle, + Current = config.GetBindable(OsuSetting.ModSelectHotkeyStyle), + ClassicDefault = ModSelectHotkeyStyle.Classic } }; } diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 4e32afb86f..84a5bf1144 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -109,15 +109,15 @@ namespace osu.Game.Overlays : base(value) { if (!(Value is Enum enumValue)) - Text.Text = Value.ToString().ToLower(); + Text.Text = Value.ToString().ToLowerInvariant(); else { var localisableDescription = enumValue.GetLocalisableDescription(); string nonLocalisableDescription = enumValue.GetDescription(); - // If localisable == non-localisable, then we must have a basic string, so .ToLower() is used. + // If localisable == non-localisable, then we must have a basic string, so .ToLowerInvariant() is used. Text.Text = localisableDescription.Equals(nonLocalisableDescription) - ? nonLocalisableDescription.ToLower() + ? nonLocalisableDescription.ToLowerInvariant() : localisableDescription; } diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 6c56f3a912..5a03d66b84 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Configuration databasedSettings.Add(setting); } - bindable.ValueChanged += b => + bindable.ValueChanged += _ => { lock (pendingWrites) pendingWrites.Add(lookup); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index e0844a5c8a..b0e83d6abe 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -1,11 +1,11 @@ // 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 Newtonsoft.Json; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// The mods which were applied to the beatmap. /// - public Mod[] Mods { get; set; } + public Mod[] Mods { get; set; } = Array.Empty(); /// /// The combined star rating of all skills. @@ -74,7 +74,8 @@ namespace osu.Game.Rulesets.Difficulty /// Reads osu-web database attribute mappings into this object. /// /// The attribute mappings. - public virtual void FromDatabaseAttributes(IReadOnlyDictionary values) + /// The where more information about the beatmap may be extracted from (such as AR/CS/OD/etc). + public virtual void FromDatabaseAttributes(IReadOnlyDictionary values, IBeatmapOnlineInfo onlineInfo) { } } diff --git a/osu.Game/Rulesets/EFRulesetInfo.cs b/osu.Game/Rulesets/EFRulesetInfo.cs index 3c671496de..bd67bdb93c 100644 --- a/osu.Game/Rulesets/EFRulesetInfo.cs +++ b/osu.Game/Rulesets/EFRulesetInfo.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets public int OnlineID { get => ID ?? -1; - set => ID = value >= 0 ? value : (int?)null; + set => ID = value >= 0 ? value : null; } #endregion diff --git a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs index cb6c4a9928..00b1ca9dc9 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Edit.Checks if (edgeType == EdgeType.None) yield break; - string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null; + string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLowerInvariant() : null; if (maxVolume <= muted_threshold) { diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs index a461f63d2d..2a222aece0 100644 --- a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Edit.Checks } } - private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith); + private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLowerInvariant().EndsWith); private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; public class IssueTemplateTooShort : IssueTemplate diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 26dd6db016..c8196b6865 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -73,6 +74,10 @@ namespace osu.Game.Rulesets.Edit /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. protected void BeginPlacement(bool commitStart = false) { + var nearestSampleControlPoint = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.SampleControlPoint?.DeepClone() as SampleControlPoint; + + HitObject.SampleControlPoint = nearestSampleControlPoint ?? new SampleControlPoint(); + placementHandler.BeginPlacement(HitObject); if (commitStart) PlacementActive = PlacementState.Active; @@ -124,10 +129,10 @@ namespace osu.Game.Rulesets.Edit switch (e) { - case ScrollEvent _: + case ScrollEvent: return false; - case DoubleClickEvent _: + case DoubleClickEvent: return false; case MouseButtonEvent mouse: diff --git a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs index 9307e36b99..599e81f2b8 100644 --- a/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs +++ b/osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods @@ -55,8 +56,8 @@ namespace osu.Game.Rulesets.Mods { base.LoadComplete(); - Current.BindValueChanged(current => updateCurrentFromSlider()); - beatmap.BindValueChanged(b => updateCurrentFromSlider(), true); + Current.BindValueChanged(_ => updateCurrentFromSlider()); + beatmap.BindValueChanged(_ => updateCurrentFromSlider(), true); sliderDisplayCurrent.BindValueChanged(number => { @@ -103,9 +104,9 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - new SettingsSlider + new OsuSliderBar { - ShowsDefaultIndicator = false, + RelativeSizeAxes = Axes.X, Current = currentNumber, KeyboardStep = 0.1f, } diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 2f5707562e..aea6e12a07 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Mods public void ApplyToDrawableHitObject(DrawableHitObject drawable) { - drawable.OnNewResult += (o, result) => + drawable.OnNewResult += (_, result) => { if (ratesForRewinding.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Mods updateTargetRate(); }; - drawable.OnRevertResult += (o, result) => + drawable.OnRevertResult += (_, result) => { if (!ratesForRewinding.ContainsKey(result.HitObject)) return; if (!shouldProcessResult(result)) return; diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index a1aad3a6b9..daa4b7c797 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mods protected ModTimeRamp() { // for preview purpose at song select. eventually we'll want to be able to update every frame. - FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true); + FinalRate.BindValueChanged(_ => applyRateAdjustment(double.PositiveInfinity), true); AdjustPitch.BindValueChanged(applyPitchAdjustment); } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 30b8fe965a..d20e0616e5 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Objects if (legacyInfo != null) DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone(); - else if (DifficultyControlPoint == DifficultyControlPoint.DEFAULT) + else if (ReferenceEquals(DifficultyControlPoint, DifficultyControlPoint.DEFAULT)) DifficultyControlPoint = new DifficultyControlPoint(); DifficultyControlPoint.Time = StartTime; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Objects // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time. if (legacyInfo != null) SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone(); - else if (SampleControlPoint == SampleControlPoint.DEFAULT) + else if (ReferenceEquals(SampleControlPoint, SampleControlPoint.DEFAULT)) SampleControlPoint = new SampleControlPoint(); SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 2285715564..ddc121eb5b 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Objects } public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null) - : this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? (PathType?)type : null)).ToArray(), expectedDistance) + : this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? type : null)).ToArray(), expectedDistance) { } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 09a090483d..020bae1c42 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Replays CurrentTime = Math.Clamp(time, frameStart, frameEnd); // In an important section, a mid-frame time cannot be used and a null is returned instead. - return inImportantSection && frameStart < time && time < frameEnd ? null : (double?)CurrentTime; + return inImportantSection && frameStart < time && time < frameEnd ? null : CurrentTime; } private double getFrameTime(int index) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 8bde71dcd7..e098472999 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -126,55 +126,55 @@ namespace osu.Game.Rulesets { switch (mod) { - case ModNoFail _: + case ModNoFail: value |= LegacyMods.NoFail; break; - case ModEasy _: + case ModEasy: value |= LegacyMods.Easy; break; - case ModHidden _: + case ModHidden: value |= LegacyMods.Hidden; break; - case ModHardRock _: + case ModHardRock: value |= LegacyMods.HardRock; break; - case ModPerfect _: + case ModPerfect: value |= LegacyMods.Perfect; break; - case ModSuddenDeath _: + case ModSuddenDeath: value |= LegacyMods.SuddenDeath; break; - case ModNightcore _: + case ModNightcore: value |= LegacyMods.Nightcore; break; - case ModDoubleTime _: + case ModDoubleTime: value |= LegacyMods.DoubleTime; break; - case ModRelax _: + case ModRelax: value |= LegacyMods.Relax; break; - case ModHalfTime _: + case ModHalfTime: value |= LegacyMods.HalfTime; break; - case ModFlashlight _: + case ModFlashlight: value |= LegacyMods.Flashlight; break; - case ModCinema _: + case ModCinema: value |= LegacyMods.Cinema; break; - case ModAutoplay _: + case ModAutoplay: value |= LegacyMods.Autoplay; break; } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index f1ed23bb21..7c37913576 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.UI { switch (e) { - case MouseDownEvent _: + case MouseDownEvent: if (mouseDisabled.Value) return true; // importantly, block upwards propagation so global bindings also don't fire. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 7ccf7a58b8..6ee1d11f83 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -27,16 +27,16 @@ namespace osu.Game.Scoring public class ScoreManager : ModelManager, IModelImporter { private readonly Scheduler scheduler; - private readonly Func difficulties; + private readonly BeatmapDifficultyCache difficultyCache; private readonly OsuConfigManager configManager; private readonly ScoreImporter scoreImporter; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, - Func difficulties = null, OsuConfigManager configManager = null) + BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null) : base(storage, realm) { this.scheduler = scheduler; - this.difficulties = difficulties; + this.difficultyCache = difficultyCache; this.configManager = configManager; scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm) @@ -65,8 +65,6 @@ namespace osu.Game.Scoring /// The given ordered by decreasing total score. public async Task OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default) { - var difficultyCache = difficulties?.Invoke(); - if (difficultyCache != null) { // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. @@ -168,11 +166,11 @@ namespace osu.Game.Scoring return score.BeatmapInfo.MaxCombo.Value; #pragma warning restore CS0618 - if (difficulties == null) + if (difficultyCache == null) return null; // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); + var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); return difficulty?.MaxCombo; } @@ -265,13 +263,13 @@ namespace osu.Game.Scoring public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks); public Live Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => - scoreImporter.Import(item, archive, batchImport, cancellationToken); + scoreImporter.ImportModel(item, archive, batchImport, cancellationToken); #region Implementation of IPresentImports - public Action>> PostImport + public Action>> PresentImport { - set => scoreImporter.PostImport = value; + set => scoreImporter.PresentImport = value; } #endregion diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs index ec1e621491..e14354222b 100644 --- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs @@ -24,6 +24,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Action.Value = () => difficultyChangeFunc.Invoke(beatmapInfo); } - public override IconUsage? GetIconForState(bool state) => state ? (IconUsage?)FontAwesome.Solid.Check : null; + public override IconUsage? GetIconForState(bool state) => state ? FontAwesome.Solid.Check : null; } } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs index 82a9db956a..55302833c1 100644 --- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons { base.LoadComplete(); - Button.Bindable.BindValueChanged(selected => updateSelectionState(), true); + Button.Bindable.BindValueChanged(_ => updateSelectionState(), true); Action = onAction; } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 4e2c018330..54ef5a2bd7 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((sender, args) => + controlPointGroups.BindCollectionChanged((_, args) => { switch (args.Action) { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts case NotifyCollectionChangedAction.Remove: foreach (var group in args.OldItems.OfType()) { - var matching = Children.SingleOrDefault(gv => gv.Group == group); + var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group)); if (matching != null) matching.Expire(); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index cccb646ba4..e058cae191 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts // Run in constructor so IsRedundant calls can work correctly. controlPoints.BindTo(Group.ControlPoints); - controlPoints.BindCollectionChanged((_, __) => + controlPoints.BindCollectionChanged((_, _) => { ClearInternal(); @@ -40,15 +40,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { switch (point) { - case TimingControlPoint _: + case TimingControlPoint: AddInternal(new ControlPointVisualisation(point) { Y = 0, }); break; - case DifficultyControlPoint _: + case DifficultyControlPoint: AddInternal(new ControlPointVisualisation(point) { Y = 0.25f, }); break; - case SampleControlPoint _: + case SampleControlPoint: AddInternal(new ControlPointVisualisation(point) { Y = 0.5f, }); break; diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index c1ae2ff8d1..54914f4b23 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); - beatmap.ValueChanged += b => + beatmap.ValueChanged += _ => { updateRelativeChildSize(); }; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 6a94fce78a..40403e08ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -293,7 +293,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); - divisorTextBox.OnCommit += (_, __) => setPresets(); + divisorTextBox.OnCommit += (_, _) => setPresets(); Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c7bf23865b..540fbf9a72 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - SelectedItems.CollectionChanged += (selectedObjects, args) => + SelectedItems.CollectionChanged += (_, args) => { switch (args.Action) { @@ -108,13 +108,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual bool AllowDeselectionDuringDrag => true; - /// - /// Positional input must be received outside the container's bounds, - /// in order to handle blueprints which are partially offscreen. - /// - /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - protected override bool OnMouseDown(MouseDownEvent e) { bool selectionPerformed = performMouseDownActions(e); diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index ba43704a5f..4c37d200bc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -39,6 +39,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private PlacementBlueprint currentPlacement; private InputManager inputManager; + /// + /// Positional input must be received outside the container's bounds, + /// in order to handle composer blueprints which are partially offscreen. + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 1c9faadda1..0bdfc5b0a0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // bring in updates from selection changes EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates); - SelectedItems.CollectionChanged += (sender, args) => Scheduler.AddOnce(UpdateTernaryStates); + SelectedItems.CollectionChanged += (_, _) => Scheduler.AddOnce(UpdateTernaryStates); } protected override void DeleteItems(IEnumerable items) => EditorBeatmap.RemoveRange(items); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 96bd352cf9..8419d3b380 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -21,6 +21,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Input; @@ -63,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { InternalChild = SelectionBox = CreateSelectionBox(); - SelectedItems.CollectionChanged += (sender, args) => + SelectedItems.CollectionChanged += (_, _) => { Scheduler.AddOnce(updateVisibility); }; @@ -103,7 +104,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Positional input must be received outside the container's bounds, /// in order to handle blueprints which are partially offscreen. /// - /// + /// + /// public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 847c96b762..648ffd9609 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -38,8 +38,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { - volume.BindValueChanged(volume => updateText()); - bank.BindValueChanged(bank => updateText(), true); + volume.BindValueChanged(_ => updateText()); + bank.BindValueChanged(_ => updateText(), true); } protected override bool OnClick(ClickEvent e) @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); // on commit, ensure that the value is correct by sourcing it from the objects' control points again. // this ensures that committing empty text causes a revert to the previous value. - bank.OnCommit += (_, __) => bank.Current.Value = getCommonBank(relevantControlPoints); + bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantControlPoints); volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); } @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null; - private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null; + private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? relevantControlPoints.First().SampleVolume : null; private void updateBankFor(IEnumerable objects, string? newBank) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 138e28fa06..742e16d5a9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -35,6 +35,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable placement; private SelectionBlueprint placementBlueprint; + /// + /// Positional input must be received outside the container's bounds, + /// in order to handle timeline blueprints which are stacked offscreen. + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ReceivePositionalInputAt(screenSpacePos); + public TimelineBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 5d5681ea2b..cfc71256e8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((sender, args) => + controlPointGroups.BindCollectionChanged((_, args) => { switch (args.Action) { @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case NotifyCollectionChangedAction.Remove: foreach (var group in args.OldItems.OfType()) { - var matching = Children.SingleOrDefault(gv => gv.Group == group); + var matching = Children.SingleOrDefault(gv => ReferenceEquals(gv.Group, group)); matching?.Expire(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 4017a745e1..10355045be 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline base.LoadComplete(); controlPoints.BindTo(Group.ControlPoints); - controlPoints.BindCollectionChanged((_, __) => + controlPoints.BindCollectionChanged((_, _) => { ClearInternal(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 34abe81ae2..32ebc9c3c1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -205,7 +205,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateRepeats(repeats); } - if (difficultyControlPoint != Item.DifficultyControlPoint) + if (!ReferenceEquals(difficultyControlPoint, Item.DifficultyControlPoint)) { difficultyControlPoint = Item.DifficultyControlPoint; difficultyOverrideDisplay?.Expire(); @@ -220,7 +220,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - if (sampleControlPoint != Item.SampleControlPoint) + if (!ReferenceEquals(sampleControlPoint, Item.SampleControlPoint)) { sampleControlPoint = Item.SampleControlPoint; sampleOverrideDisplay?.Expire(); @@ -393,7 +393,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (e.CurrentState.Keyboard.ShiftPressed) { - if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT) + if (ReferenceEquals(hitObject.DifficultyControlPoint, DifficultyControlPoint.DEFAULT)) hitObject.DifficultyControlPoint = new DifficultyControlPoint(); double newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration); diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index ef670c9fcf..3d18b00e75 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Edit.Compose if (composer == null) return; - EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability()); + EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateClipboardActionAvailability()); clipboard.BindValueChanged(_ => updateClipboardActionAvailability()); composer.OnLoadComplete += _ => updateClipboardActionAvailability(); updateClipboardActionAvailability(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d0d4c0ba1e..81ae08aea1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -21,6 +21,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -39,6 +40,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; @@ -97,6 +99,30 @@ namespace osu.Game.Screens.Edit public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + /// + /// Ensure all asynchronously loading pieces of the editor are in a good state. + /// This exists here for convenience for tests, not for actual use. + /// Eventually we'd probably want a better way to signal this. + /// + public bool ReadyForUse + { + get + { + if (!workingBeatmapUpdated) + return false; + + if (currentScreen?.IsLoaded != true) + return false; + + if (currentScreen is EditorScreenWithTimeline) + return currentScreen.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + + return true; + } + } + + private bool workingBeatmapUpdated; + private readonly Bindable samplePlaybackDisabled = new Bindable(); private bool canSave; @@ -219,6 +245,7 @@ namespace osu.Game.Screens.Edit // this assumes that nothing during the rest of this load() method is accessing Beatmap.Value (loadableBeatmap should be preferred). // generally this is quite safe, as the actual load of editor content comes after menuBar.Mode.ValueChanged is fired in its own LoadComplete. Beatmap.Value = loadableBeatmap; + workingBeatmapUpdated = true; }); OsuMenuItem undoMenuItem; @@ -798,10 +825,11 @@ namespace osu.Game.Screens.Edit if (trackPlaying) { - // generally users are not looking to perform tiny seeks when the track is playing, - // so seeks should always be by one full beat, bypassing the beatDivisor. + // generally users are not looking to perform tiny seeks when the track is playing. // this multiplication undoes the division that will be applied in the underlying seek operation. - amount *= beatDivisor.Value; + // scale by BPM to keep the seek amount constant across all BPMs. + var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(clock.CurrentTimeAccurate); + amount *= beatDivisor.Value * (timingPoint.BPM / 120); } if (direction < 1) @@ -907,6 +935,6 @@ namespace osu.Game.Screens.Edit ControlPointInfo IBeatSyncProvider.ControlPoints => editorBeatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => clock; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null; } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index fa3cb51473..96425e8bc8 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -73,31 +73,7 @@ namespace osu.Game.Screens.Edit public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null) { PlayableBeatmap = playableBeatmap; - - // ensure we are not working with legacy control points. - // if we leave the legacy points around they will be applied over any local changes on - // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter. - if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo) - { - var newControlPoints = new ControlPointInfo(); - - foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints) - { - switch (controlPoint) - { - case DifficultyControlPoint _: - case SampleControlPoint _: - // skip legacy types. - continue; - - default: - newControlPoints.Add(controlPoint.Time, controlPoint); - break; - } - } - - playableBeatmap.ControlPointInfo = newControlPoints; - } + PlayableBeatmap.ControlPointInfo = ConvertControlPoints(PlayableBeatmap.ControlPointInfo); this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; @@ -110,10 +86,43 @@ namespace osu.Game.Screens.Edit trackStartTime(obj); } + /// + /// Converts a such that the resultant is non-legacy. + /// + /// The to convert. + /// The non-legacy . is returned if already non-legacy. + public static ControlPointInfo ConvertControlPoints(ControlPointInfo incoming) + { + // ensure we are not working with legacy control points. + // if we leave the legacy points around they will be applied over any local changes on + // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter. + if (!(incoming is LegacyControlPointInfo)) + return incoming; + + var newControlPoints = new ControlPointInfo(); + + foreach (var controlPoint in incoming.AllControlPoints) + { + switch (controlPoint) + { + case DifficultyControlPoint: + case SampleControlPoint: + // skip legacy types. + continue; + + default: + newControlPoints.Add(controlPoint.Time, controlPoint); + break; + } + } + + return newControlPoints; + } + public BeatmapInfo BeatmapInfo { get => beatmapInfo; - set => throw new InvalidOperationException(); + set => throw new InvalidOperationException($"Can't set {nameof(BeatmapInfo)} on {nameof(EditorBeatmap)}"); } public BeatmapMetadata Metadata => beatmapInfo.Metadata; diff --git a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs index 244fd5f1e9..475c894b30 100644 --- a/osu.Game/Screens/Edit/EditorBeatmapSkin.cs +++ b/osu.Game/Screens/Edit/EditorBeatmapSkin.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit ComboColours = new BindableList(); if (Skin.Configuration.ComboColours != null) ComboColours.AddRange(Skin.Configuration.ComboColours.Select(c => (Colour4)c)); - ComboColours.BindCollectionChanged((_, __) => updateColours()); + ComboColours.BindCollectionChanged((_, _) => updateColours()); } private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke(); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index c7df36be77..c3b29afd30 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit seekTime = timingPoint.Time + closestBeat * seekAmount; } - if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) + if (seekTime < timingPoint.Time && !ReferenceEquals(timingPoint, ControlPointInfo.TimingPoints.First())) seekTime = timingPoint.Time; SeekSmoothlyTo(seekTime); diff --git a/osu.Game/Screens/Edit/EditorLoader.cs b/osu.Game/Screens/Edit/EditorLoader.cs index 702e22a272..d6af990b52 100644 --- a/osu.Game/Screens/Edit/EditorLoader.cs +++ b/osu.Game/Screens/Edit/EditorLoader.cs @@ -65,6 +65,8 @@ namespace osu.Game.Screens.Edit base.LoadComplete(); // will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`. + if (!(Beatmap.Value is DummyWorkingBeatmap)) + Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset; Mods.Value = Array.Empty(); } diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index 8765dae384..a290cce708 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { - hoveredBackground.Colour = colourHover = colours.Background1; + colourHover = colours.Background1; colourSelected = colours.Colour3; } @@ -105,8 +105,7 @@ namespace osu.Game.Screens.Edit { base.LoadComplete(); - // Reduce flicker of rows when offset is being changed rapidly. - // Probably need to reconsider this. + updateState(); FinishTransforms(true); } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 4e3b6e0aee..b4647c2b64 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -5,13 +5,16 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using DiffPlex; +using DiffPlex.Model; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Skinning; using Decoder = osu.Game.Beatmaps.Formats.Decoder; @@ -34,61 +37,107 @@ namespace osu.Game.Screens.Edit { // Diff the beatmaps var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false); + IBeatmap newBeatmap = null; - // Find the index of [HitObject] sections. Lines changed prior to this index are ignored. - int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]"); - int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]"); + editorBeatmap.BeginChange(); + processHitObjects(result, () => newBeatmap ??= readBeatmap(newState)); + processTimingPoints(() => newBeatmap ??= readBeatmap(newState)); + editorBeatmap.EndChange(); + } - Debug.Assert(oldHitObjectsIndex >= 0); - Debug.Assert(newHitObjectsIndex >= 0); + private void processTimingPoints(Func getNewBeatmap) + { + ControlPointInfo newControlPoints = EditorBeatmap.ConvertControlPoints(getNewBeatmap().ControlPointInfo); - var toRemove = new List(); - var toAdd = new List(); + // Remove all groups from the current beatmap which don't have a corresponding equal group in the new beatmap. + foreach (var oldGroup in editorBeatmap.ControlPointInfo.Groups.ToArray()) + { + var newGroup = newControlPoints.GroupAt(oldGroup.Time); + + if (!oldGroup.Equals(newGroup)) + editorBeatmap.ControlPointInfo.RemoveGroup(oldGroup); + } + + // Add all groups from the new beatmap which don't have a corresponding equal group in the old beatmap. + foreach (var newGroup in newControlPoints.Groups) + { + var oldGroup = editorBeatmap.ControlPointInfo.GroupAt(newGroup.Time); + + if (!newGroup.Equals(oldGroup)) + { + foreach (var point in newGroup.ControlPoints) + editorBeatmap.ControlPointInfo.Add(newGroup.Time, point); + } + } + } + + private void processHitObjects(DiffResult result, Func getNewBeatmap) + { + findChangedIndices(result, LegacyDecoder.Section.HitObjects, out var removedIndices, out var addedIndices); + + for (int i = removedIndices.Count - 1; i >= 0; i--) + editorBeatmap.RemoveAt(removedIndices[i]); + + if (addedIndices.Count > 0) + { + var newBeatmap = getNewBeatmap(); + + foreach (int i in addedIndices) + editorBeatmap.Insert(i, newBeatmap.HitObjects[i]); + } + } + + private void findChangedIndices(DiffResult result, LegacyDecoder.Section section, out List removedIndices, out List addedIndices) + { + removedIndices = new List(); + addedIndices = new List(); + + // Find the start and end indices of the relevant section headers in both the old and the new beatmap file. Lines changed outside of the modified ranges are ignored. + int oldSectionStartIndex = Array.IndexOf(result.PiecesOld, $"[{section}]"); + if (oldSectionStartIndex == -1) + return; + + int oldSectionEndIndex = Array.FindIndex(result.PiecesOld, oldSectionStartIndex + 1, s => s.StartsWith('[')); + if (oldSectionEndIndex == -1) + oldSectionEndIndex = result.PiecesOld.Length; + + int newSectionStartIndex = Array.IndexOf(result.PiecesNew, $"[{section}]"); + if (newSectionStartIndex == -1) + return; + + int newSectionEndIndex = Array.FindIndex(result.PiecesNew, newSectionStartIndex + 1, s => s.StartsWith('[')); + if (newSectionEndIndex == -1) + newSectionEndIndex = result.PiecesNew.Length; foreach (var block in result.DiffBlocks) { - // Removed hitobjects + // Removed indices for (int i = 0; i < block.DeleteCountA; i++) { - int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1; + int objectIndex = block.DeleteStartA + i; - if (hoIndex < 0) + if (objectIndex <= oldSectionStartIndex || objectIndex >= oldSectionEndIndex) continue; - toRemove.Add(hoIndex); + removedIndices.Add(objectIndex - oldSectionStartIndex - 1); } - // Added hitobjects + // Added indices for (int i = 0; i < block.InsertCountB; i++) { - int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1; + int objectIndex = block.InsertStartB + i; - if (hoIndex < 0) + if (objectIndex <= newSectionStartIndex || objectIndex >= newSectionEndIndex) continue; - toAdd.Add(hoIndex); + addedIndices.Add(objectIndex - newSectionStartIndex - 1); } } // Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion. // This isn't strictly required, but the differ makes no guarantees about order. - toRemove.Sort(); - toAdd.Sort(); - - editorBeatmap.BeginChange(); - - // Apply the changes. - for (int i = toRemove.Count - 1; i >= 0; i--) - editorBeatmap.RemoveAt(toRemove[i]); - - if (toAdd.Count > 0) - { - IBeatmap newBeatmap = readBeatmap(newState); - foreach (int i in toAdd) - editorBeatmap.Insert(i, newBeatmap.HitObjects[i]); - } - - editorBeatmap.EndChange(); + removedIndices.Sort(); + addedIndices.Sort(); } private string readString(byte[] state) => Encoding.UTF8.GetString(state); diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs index 12a6843067..40bbfeaf7d 100644 --- a/osu.Game/Screens/Edit/Setup/DesignSection.cs +++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Setup EnableCountdown.Current.BindValueChanged(_ => updateBeatmap()); CountdownSpeed.Current.BindValueChanged(_ => updateBeatmap()); - CountdownOffset.OnCommit += (_, __) => onOffsetCommitted(); + CountdownOffset.OnCommit += (_, _) => onOffsetCommitted(); widescreenSupport.Current.BindValueChanged(_ => updateBeatmap()); epilepsyWarning.Current.BindValueChanged(_ => updateBeatmap()); diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs deleted file mode 100644 index c13026a891..0000000000 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ /dev/null @@ -1,92 +0,0 @@ -// 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.IO; -using System.Linq; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Database; -using osu.Game.Graphics.UserInterfaceV2; -using osuTK; - -namespace osu.Game.Screens.Edit.Setup -{ - /// - /// A labelled textbox which reveals an inline file chooser when clicked. - /// - internal class FileChooserLabelledTextBox : LabelledTextBoxWithPopover, ICanAcceptFiles - { - private readonly string[] handledExtensions; - - public IEnumerable HandledExtensions => handledExtensions; - - private readonly Bindable currentFile = new Bindable(); - - [Resolved] - private OsuGameBase game { get; set; } = null!; - - public FileChooserLabelledTextBox(params string[] handledExtensions) - { - this.handledExtensions = handledExtensions; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - game.RegisterImportHandler(this); - currentFile.BindValueChanged(onFileSelected); - } - - private void onFileSelected(ValueChangedEvent file) - { - if (file.NewValue == null) - return; - - this.HidePopover(); - Current.Value = file.NewValue.FullName; - } - - Task ICanAcceptFiles.Import(params string[] paths) - { - Schedule(() => currentFile.Value = new FileInfo(paths.First())); - return Task.CompletedTask; - } - - Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException(); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (game.IsNotNull()) - game.UnregisterImportHandler(this); - } - - public override Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile); - - private class FileChooserPopover : OsuPopover - { - public FileChooserPopover(string[] handledExtensions, Bindable currentFile) - { - Child = new Container - { - Size = new Vector2(600, 400), - Child = new OsuFileSelector(currentFile.Value?.DirectoryName, handledExtensions) - { - RelativeSizeAxes = Axes.Both, - CurrentFile = { BindTarget = currentFile } - }, - }; - } - } - } -} diff --git a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs new file mode 100644 index 0000000000..efc8b6978e --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs @@ -0,0 +1,131 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.Edit.Setup +{ + /// + /// A labelled drawable displaying file chooser on click, with placeholder text support. + /// todo: this should probably not use PopoverTextBox just to display placeholder text, but is the best way for now. + /// + internal class LabelledFileChooser : LabelledDrawable, IHasCurrentValue, ICanAcceptFiles, IHasPopover + { + private readonly string[] handledExtensions; + + public IEnumerable HandledExtensions => handledExtensions; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + /// + /// The initial path to use when displaying the . + /// + /// + /// Uses a value before the first selection is made + /// to ensure that the first selection starts at . + /// + private string? initialChooserPath; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + public LocalisableString Text + { + get => Component.PlaceholderText; + set => Component.PlaceholderText = value; + } + + public CompositeDrawable TabbableContentContainer + { + set => Component.TabbableContentContainer = value; + } + + public LabelledFileChooser(params string[] handledExtensions) + : base(false) + { + this.handledExtensions = handledExtensions; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + game.RegisterImportHandler(this); + Current.BindValueChanged(onFileSelected); + } + + private void onFileSelected(ValueChangedEvent file) + { + if (file.NewValue != null) + this.HidePopover(); + + initialChooserPath = file.NewValue?.DirectoryName; + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => Current.Value = new FileInfo(paths.First())); + return Task.CompletedTask; + } + + Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException(); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (game.IsNotNull()) + game.UnregisterImportHandler(this); + } + + protected override LabelledTextBoxWithPopover.PopoverTextBox CreateComponent() => new LabelledTextBoxWithPopover.PopoverTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + OnFocused = this.ShowPopover, + }; + + public Popover GetPopover() => new FileChooserPopover(handledExtensions, Current, initialChooserPath); + + private class FileChooserPopover : OsuPopover + { + public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + { + Child = new Container + { + Size = new Vector2(600, 400), + Child = new OsuFileSelector(chooserPath, handledExtensions) + { + RelativeSizeAxes = Axes.Both, + CurrentFile = { BindTarget = currentFile } + }, + }; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 7c487d3bf8..1f8381e1ed 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -10,15 +10,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Setup { internal class ResourcesSection : SetupSection { - private LabelledTextBox audioTrackTextBox; - private LabelledTextBox backgroundTextBox; + private LabelledFileChooser audioTrackChooser; + private LabelledFileChooser backgroundChooser; public override LocalisableString Title => "Resources"; @@ -42,76 +41,81 @@ namespace osu.Game.Screens.Edit.Setup { Children = new Drawable[] { - backgroundTextBox = new FileChooserLabelledTextBox(".jpg", ".jpeg", ".png") + backgroundChooser = new LabelledFileChooser(".jpg", ".jpeg", ".png") { Label = "Background", FixedLabelWidth = LABEL_WIDTH, - PlaceholderText = "Click to select a background image", - Current = { Value = working.Value.Metadata.BackgroundFile }, TabbableContentContainer = this }, - audioTrackTextBox = new FileChooserLabelledTextBox(".mp3", ".ogg") + audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg") { Label = "Audio Track", FixedLabelWidth = LABEL_WIDTH, - PlaceholderText = "Click to select a track", - Current = { Value = working.Value.Metadata.AudioFile }, TabbableContentContainer = this }, }; - backgroundTextBox.Current.BindValueChanged(backgroundChanged); - audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); + if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile)) + backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile); + + if (!string.IsNullOrEmpty(working.Value.Metadata.AudioFile)) + audioTrackChooser.Current.Value = new FileInfo(working.Value.Metadata.AudioFile); + + backgroundChooser.Current.BindValueChanged(backgroundChanged); + audioTrackChooser.Current.BindValueChanged(audioTrackChanged); + + updatePlaceholderText(); } - public bool ChangeBackgroundImage(string path) + public bool ChangeBackgroundImage(FileInfo source) { - var info = new FileInfo(path); - - if (!info.Exists) + if (!source.Exists) return false; var set = working.Value.BeatmapSetInfo; + var destination = new FileInfo($@"bg{source.Extension}"); + // 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); - using (var stream = info.OpenRead()) + using (var stream = source.OpenRead()) { if (oldFile != null) beatmaps.DeleteFile(set, oldFile); - beatmaps.AddFile(set, stream, info.Name); + beatmaps.AddFile(set, stream, destination.Name); } - working.Value.Metadata.BackgroundFile = info.Name; + working.Value.Metadata.BackgroundFile = destination.Name; header.Background.UpdateBackground(); return true; } - public bool ChangeAudioTrack(string path) + public bool ChangeAudioTrack(FileInfo source) { - var info = new FileInfo(path); - - if (!info.Exists) + if (!source.Exists) return false; var set = working.Value.BeatmapSetInfo; + var destination = new FileInfo($@"audio{source.Extension}"); + // 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); - using (var stream = info.OpenRead()) + using (var stream = source.OpenRead()) { if (oldFile != null) beatmaps.DeleteFile(set, oldFile); - beatmaps.AddFile(set, stream, info.Name); + + beatmaps.AddFile(set, stream, destination.Name); } - working.Value.Metadata.AudioFile = info.Name; + working.Value.Metadata.AudioFile = destination.Name; music.ReloadCurrentTrack(); @@ -119,16 +123,31 @@ namespace osu.Game.Screens.Edit.Setup return true; } - private void backgroundChanged(ValueChangedEvent filePath) + private void backgroundChanged(ValueChangedEvent file) { - if (!ChangeBackgroundImage(filePath.NewValue)) - backgroundTextBox.Current.Value = filePath.OldValue; + if (!ChangeBackgroundImage(file.NewValue)) + backgroundChooser.Current.Value = file.OldValue; + + updatePlaceholderText(); } - private void audioTrackChanged(ValueChangedEvent filePath) + private void audioTrackChanged(ValueChangedEvent file) { - if (!ChangeAudioTrack(filePath.NewValue)) - audioTrackTextBox.Current.Value = filePath.OldValue; + if (!ChangeAudioTrack(file.NewValue)) + audioTrackChooser.Current.Value = file.OldValue; + + updatePlaceholderText(); + } + + private void updatePlaceholderText() + { + audioTrackChooser.Text = audioTrackChooser.Current.Value == null + ? "Click to select a track" + : "Click to replace the track"; + + backgroundChooser.Text = backgroundChooser.Current.Value == null + ? "Click to select a background image" + : "Click to replace the background image"; } } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 88c738c1d2..b51da2c53d 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.Edit.Timing Columns = createHeaders(); Content = value.Select(createContent).ToArray().ToRectangular(); + + updateSelectedGroup(); } } @@ -61,13 +63,20 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(group => + selectedGroup.BindValueChanged(_ => { // TODO: This should scroll the selected row into view. - foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue; + updateSelectedGroup(); }, true); } + private void updateSelectedGroup() + { + // TODO: This should scroll the selected row into view. + foreach (var b in BackgroundFlow) + b.Selected = ReferenceEquals(b.Item, selectedGroup?.Value); + } + private TableColumn[] createHeaders() { var columns = new List @@ -144,7 +153,7 @@ namespace osu.Game.Screens.Edit.Timing protected override void LoadComplete() { base.LoadComplete(); - controlPoints.CollectionChanged += (_, __) => createChildren(); + controlPoints.CollectionChanged += (_, _) => createChildren(); } private void createChildren() diff --git a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs index d05777e793..998e49a6ab 100644 --- a/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs +++ b/osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Timing base.LoadComplete(); Current.BindValueChanged(_ => updateFromCurrent(), true); - numeratorBox.OnCommit += (_, __) => updateFromNumeratorBox(); + numeratorBox.OnCommit += (_, _) => updateFromNumeratorBox(); } private void updateFromCurrent() diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 270d544dcf..3895959982 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -14,6 +15,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; @@ -35,17 +37,29 @@ namespace osu.Game.Screens.Edit.Timing private IAdjustableClock metronomeClock; - private Sample clunk; + private Sample sampleTick; + private Sample sampleTickDownbeat; + private Sample sampleLatch; + + [CanBeNull] + private ScheduledDelegate tickPlaybackDelegate; [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } public bool EnableClicking { get; set; } = true; + public MetronomeDisplay() + { + AllowMistimedEventFiring = false; + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { - clunk = audio.Samples.Get(@"Multiplayer/countdown-tick"); + sampleTick = audio.Samples.Get(@"UI/metronome-tick"); + sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat"); + sampleLatch = audio.Samples.Get(@"UI/metronome-latch"); const float taper = 25; const float swing_vertical_offset = -23; @@ -217,6 +231,8 @@ namespace osu.Game.Screens.Edit.Timing private readonly BindableInt interpolatedBpm = new BindableInt(); + private ScheduledDelegate latchDelegate; + protected override void LoadComplete() { base.LoadComplete(); @@ -251,13 +267,28 @@ namespace osu.Game.Screens.Edit.Timing { swing.ClearTransforms(true); - using (swing.BeginDelayedSequence(350)) + isSwinging = false; + + tickPlaybackDelegate?.Cancel(); + tickPlaybackDelegate = null; + + // instantly latch if pendulum arm is close enough to center (to prevent awkward delayed playback of latch sound) + if (Precision.AlmostEquals(swing.Rotation, 0, 1)) + { + swing.RotateTo(0, 60, Easing.OutQuint); + stick.FadeColour(overlayColourProvider.Colour2, 1000, Easing.OutQuint); + sampleLatch?.Play(); + return; + } + + using (BeginDelayedSequence(350)) { swing.RotateTo(0, 1000, Easing.OutQuint); stick.FadeColour(overlayColourProvider.Colour2, 1000, Easing.OutQuint); - } - isSwinging = false; + using (BeginDelayedSequence(380)) + latchDelegate = Schedule(() => sampleLatch?.Play()); + } } } @@ -272,6 +303,9 @@ namespace osu.Game.Screens.Edit.Timing isSwinging = true; + latchDelegate?.Cancel(); + latchDelegate = null; + float currentAngle = swing.Rotation; float targetAngle = currentAngle > 0 ? -angle : angle; @@ -283,18 +317,18 @@ namespace osu.Game.Screens.Edit.Timing { stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); - Schedule(() => + tickPlaybackDelegate = Schedule(() => { if (!EnableClicking) return; - var channel = clunk?.GetChannel(); + var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel(); - if (channel != null) - { - channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); - channel.Play(); - } + if (channel == null) + return; + + channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); + channel.Play(); }); } } diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index 6a804c7e3e..4dcb5ad2ba 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing Current.TriggerChange(); }; - Current.BindValueChanged(val => + Current.BindValueChanged(_ => { decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index 53c92bac20..abc73851e4 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -195,6 +195,8 @@ namespace osu.Game.Screens.Edit.Timing private void adjustOffset(double adjust) { + bool wasAtStart = editorClock.CurrentTimeAccurate == selectedGroup.Value.Time; + // VERY TEMPORARY var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray(); @@ -208,7 +210,7 @@ namespace osu.Game.Screens.Edit.Timing // the control point might not necessarily exist yet, if currentGroupItems was empty. selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true); - if (!editorClock.IsRunning) + if (!editorClock.IsRunning && wasAtStart) editorClock.Seek(newOffset); } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 60cb263a79..fd218209d4 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Edit.Timing }, true); controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((sender, args) => + controlPointGroups.BindCollectionChanged((_, _) => { table.ControlGroups = controlPointGroups; changeHandler?.SaveState(); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Edit.Timing // Try and create matching types from the currently selected control point. var selected = selectedGroup.Value; - if (selected != null && selected != group) + if (selected != null && !ReferenceEquals(selected, group)) { foreach (var controlPoint in selected.ControlPoints) { diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 8413409d84..9b86969db1 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Timing { Label = "BPM"; - OnCommit += (val, isNew) => + OnCommit += (_, isNew) => { if (!isNew) return; diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index 67b7abb25b..2956a28547 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(_ => updateTimingGroup(), true); controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups); - controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup()); + controlPointGroups.BindCollectionChanged((_, _) => updateTimingGroup()); beatLength.BindValueChanged(_ => regenerateDisplay(true), true); @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing double? offsetChange = newStartTime - selectedGroupStartTime; var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints - .SkipWhile(g => g != tcp) + .SkipWhile(g => !ReferenceEquals(g, tcp)) .Skip(1) .FirstOrDefault(); diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 7ce70b4b5f..bffda4ec41 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Edit.Verify base.LoadComplete(); verify.InterpretedDifficulty.BindValueChanged(_ => refresh()); - verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh()); + verify.HiddenIssueTypes.BindCollectionChanged((_, _) => refresh()); refresh(); } diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index d5b32760c9..3030018138 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Verify [BackgroundDependencyLoader] private void load() { - InterpretedDifficulty.Default = BeatmapDifficultyCache.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating); + InterpretedDifficulty.Default = StarDifficulty.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating); InterpretedDifficulty.SetDefault(); Child = new Container diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index f77123af52..a81658a4b6 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Menu .Then(5500) .FadeOut(250) .ScaleTo(0.9f, 250, Easing.InQuint) - .Finally(d => + .Finally(_ => { if (nextScreen != null) this.Push(nextScreen); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 766b5bf2a4..c81195bbd3 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -19,7 +19,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -141,7 +140,7 @@ namespace osu.Game.Screens.Menu { // if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state. // this could happen if a user has nuked their files store. for now, reimport to repair this. - var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely(); + var import = beatmaps.Import(new ImportTask(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely(); import?.PerformWrite(b => b.Protected = true); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 6a74a6bd6e..ba63902b46 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -302,6 +303,8 @@ namespace osu.Game.Screens.Menu public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) { + Logger.Log($"{nameof(MainMenu)} completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {ruleset}"); + Beatmap.Value = beatmap; Ruleset.Value = ruleset; diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index 1dc05def0f..39a887f820 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Components [BackgroundDependencyLoader] private void load() { - Playlist.CollectionChanged += (_, __) => updateText(); + Playlist.CollectionChanged += (_, _) => updateText(); updateText(); } diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index bed55e8a79..9a48769405 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Components switch (tab) { - case BeatmapDetailAreaPlaylistTabItem _: + case BeatmapDetailAreaPlaylistTabItem: playlistArea.Show(); break; diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs deleted file mode 100644 index 641776a9e8..0000000000 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ /dev/null @@ -1,78 +0,0 @@ -// 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.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Rulesets; -using osuTK; - -namespace osu.Game.Screens.OnlinePlay.Components -{ - public class ModeTypeInfo : OnlinePlayComposite - { - private const float height = 28; - private const float transition_duration = 100; - - [Resolved] - private RulesetStore rulesets { get; set; } - - private Container drawableRuleset; - - public ModeTypeInfo() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - Container gameTypeContainer; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - LayoutDuration = 100, - Children = new[] - { - drawableRuleset = new Container - { - AutoSizeAxes = Axes.Both, - }, - gameTypeContainer = new Container - { - AutoSizeAxes = Axes.Both, - }, - }, - }; - - Type.BindValueChanged(type => gameTypeContainer.Child = new DrawableGameType(type.NewValue) { Size = new Vector2(height) }, true); - - Playlist.CollectionChanged += (_, __) => updateBeatmap(); - - updateBeatmap(); - } - - private void updateBeatmap() - { - var item = Playlist.FirstOrDefault(); - var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - - if (item?.Beatmap != null && ruleset != null) - { - var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); - - drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; - } - else - drawableRuleset.FadeOut(transition_duration); - } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index 795f776496..fe89eaf591 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Components InternalChild = sprite = CreateBackgroundSprite(); CurrentPlaylistItem.BindValueChanged(_ => updateBeatmap()); - Playlist.CollectionChanged += (_, __) => updateBeatmap(); + Playlist.CollectionChanged += (_, _) => updateBeatmap(); updateBeatmap(); } diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index 43ad5e97ec..a268d4d917 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { base.LoadComplete(); - Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration(), true); + Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(), true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index d8515d2bd2..2f38321e13 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Components [BackgroundDependencyLoader] private void load() { - RecentParticipants.CollectionChanged += (_, __) => updateParticipants(); + RecentParticipants.CollectionChanged += (_, _) => updateParticipants(); updateParticipants(); } diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs index 4cf785ce60..b6ef080e44 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Components base.LoadComplete(); DifficultyRange.BindValueChanged(_ => updateRange()); - Playlist.BindCollectionChanged((_, __) => updateRange(), true); + Playlist.BindCollectionChanged((_, _) => updateRange(), true); } private void updateRange() diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 489bf128e8..5193fe5cbf 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -181,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay // schedules added as the properties may change value while the drawable items haven't been created yet. SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(scrollToSelection)); - Items.BindCollectionChanged((_, __) => Scheduler.AddOnce(scrollToSelection), true); + Items.BindCollectionChanged((_, _) => Scheduler.AddOnce(scrollToSelection), true); } private void scrollToSelection() diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 53519b8b00..a3a176477e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -266,7 +266,7 @@ namespace osu.Game.Screens.OnlinePlay } if (beatmap != null) - difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index c27b78642a..0f02692eda 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; -using osuTK.Input; namespace osu.Game.Screens.OnlinePlay { @@ -33,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay IsValidMod = _ => true; } - protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys); + protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); protected override IEnumerable CreateFooterButtons() => base.CreateFooterButtons().Prepend( new SelectAllModsButton(this) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index 06befe0971..a9e9f046e2 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -38,8 +38,8 @@ namespace osu.Game.Screens.OnlinePlay }; // unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to. - stack.ScreenPushed += (_, __) => updateSubScreenTitle(); - stack.ScreenExited += (_, __) => updateSubScreenTitle(); + stack.ScreenPushed += (_, _) => updateSubScreenTitle(); + stack.ScreenExited += (_, _) => updateSubScreenTitle(); } private void updateSubScreenTitle() => title.Screen = stack.CurrentScreen as IOnlinePlaySubScreen; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs index 5bdd24221c..474463d5f1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistCountPill.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components base.LoadComplete(); PlaylistItemStats.BindValueChanged(_ => updateCount()); - Playlist.BindCollectionChanged((_, __) => updateCount(), true); + Playlist.BindCollectionChanged((_, _) => updateCount(), true); } private void updateCount() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 9784e5aace..8a2aeb9e5e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -250,7 +250,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge base.LoadComplete(); ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox)); - passwordTextBox.OnCommit += (_, __) => performJoin(); + passwordTextBox.OnCommit += (_, _) => performJoin(); } private void performJoin() diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs index d81fee24e0..58ccb24f7a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeBackgroundScreen.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public LoungeBackgroundScreen() { SelectedRoom.BindValueChanged(onSelectedRoomChanged); - playlist.BindCollectionChanged((_, __) => PlaylistItem = playlist.GetCurrentItem()); + playlist.BindCollectionChanged((_, _) => PlaylistItem = playlist.GetCurrentItem()); } private void onSelectedRoomChanged(ValueChangedEvent room) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f7f3c27ede..09bc496da5 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -304,7 +304,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); - RoomManager?.JoinRoom(room, password, r => + RoomManager?.JoinRoom(room, password, _ => { Open(room); joiningRoomOperation?.Dispose(); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs index c295e9f85e..71447b15e2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomSettingsOverlay.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), - Text = title.ToUpper(), + Text = title.ToUpperInvariant(), }, content = new Container { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 20762b2f22..c5d6da1ebc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match switch (room?.Countdown) { - case MatchStartCountdown _: + case MatchStartCountdown: newCountdown = room.Countdown; break; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index b5bb4a766f..b55a7d0731 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { var clickOperation = ongoingOperationTracker.BeginOperation(); - Client.ToggleSpectate().ContinueWith(t => endOperation()); + Client.ToggleSpectate().ContinueWith(_ => endOperation()); void endOperation() => clickOperation?.Dispose(); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs index 9877c15f2f..f7abc91227 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylistTabControl.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - QueueItems.BindCollectionChanged((_, __) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); + QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 04342788db..39740e650f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist protected override void LoadComplete() { base.LoadComplete(); - roomPlaylist.BindCollectionChanged((_, __) => InvalidateLayout()); + roomPlaylist.BindCollectionChanged((_, _) => InvalidateLayout()); } public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index ae65e1d969..d49f894ad7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void load() { isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(c => Scheduler.AddOnce(poll), true); + isConnected.BindValueChanged(_ => Scheduler.AddOnce(poll), true); } private void poll() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 495d3f94e1..dbd679104e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -1,11 +1,12 @@ // 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 System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; @@ -20,11 +21,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public class MultiplayerMatchSongSelect : OnlinePlaySongSelect { [Resolved] - private MultiplayerClient client { get; set; } + private MultiplayerClient client { get; set; } = null!; + [Resolved] + private OngoingOperationTracker operationTracker { get; set; } = null!; + + private readonly IBindable operationInProgress = new Bindable(); private readonly long? itemToEdit; - private LoadingLayer loadingLayer; + private LoadingLayer loadingLayer = null!; + private IDisposable? selectionOperation; /// /// Construct a new instance of multiplayer song select. @@ -33,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer /// The item to be edited. May be null, in which case a new item will be added to the playlist. /// An optional initial beatmap selection to perform. /// An optional initial ruleset selection to perform. - public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap? beatmap = null, RulesetInfo? ruleset = null) : base(room) { this.itemToEdit = itemToEdit; @@ -54,13 +60,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AddInternal(loadingLayer = new LoadingLayer(true)); } - protected override void SelectItem(PlaylistItem item) + protected override void LoadComplete() { + base.LoadComplete(); + + operationInProgress.BindTo(operationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); + } + + private void updateLoadingLayer() + { + if (operationInProgress.Value) + loadingLayer.Show(); + else + loadingLayer.Hide(); + } + + protected override bool SelectItem(PlaylistItem item) + { + if (operationInProgress.Value) + { + Logger.Log($"{nameof(SelectedItem)} aborted due to {nameof(operationInProgress)}"); + return false; + } + // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) { - loadingLayer.Show(); + selectionOperation = operationTracker.BeginOperation(); var multiplayerItem = new MultiplayerPlaylistItem { @@ -74,18 +102,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); - task.FireAndForget(onSuccess: () => Schedule(() => + task.FireAndForget(onSuccess: () => { - loadingLayer.Hide(); + selectionOperation.Dispose(); - // If an error or server side trigger occurred this screen may have already exited by external means. - if (this.IsCurrentScreen()) - this.Exit(); - }), onError: _ => Schedule(() => + Schedule(() => + { + // If an error or server side trigger occurred this screen may have already exited by external means. + if (this.IsCurrentScreen()) + this.Exit(); + }); + }, onError: _ => { - loadingLayer.Hide(); - Carousel.AllowSelection = true; - })); + selectionOperation.Dispose(); + + Schedule(() => + { + Carousel.AllowSelection = true; + }); + }); } else { @@ -93,6 +128,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Playlist.Add(item); this.Exit(); } + + return true; } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index df0f94688b..a82da7b185 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.GameplayStarted += onGameplayStarted; client.ResultsReady += onResultsReady; - ScoreProcessor.HasCompleted.BindValueChanged(completed => + ScoreProcessor.HasCompleted.BindValueChanged(_ => { // wait for server to tell us that results are ready (see SubmitScore implementation) loadingDisplay.Show(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index e048c12686..8270bb3b6f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users) { Expanded = { Value = true }, - }, l => + }, _ => { foreach (var instance in instances) leaderboard.AddClock(instance.UserId, instance.GameplayClock); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 1d9ea5f77d..0bf5f2604c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.OnlinePlay base.LoadComplete(); subScreenSelectedItem?.BindValueChanged(_ => UpdateSelectedItem()); - Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); + Playlist.BindCollectionChanged((_, _) => UpdateSelectedItem(), true); } protected void UpdateSelectedItem() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1552811c74..f26480909e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -118,8 +118,6 @@ namespace osu.Game.Screens.OnlinePlay protected sealed override bool OnStart() { - itemSelected = true; - var item = new PlaylistItem(Beatmap.Value.BeatmapInfo) { RulesetID = Ruleset.Value.OnlineID, @@ -127,15 +125,21 @@ namespace osu.Game.Screens.OnlinePlay AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - SelectItem(item); - return true; + if (SelectItem(item)) + { + itemSelected = true; + return true; + } + + return false; } /// /// Invoked when the user has requested a selection of a beatmap. /// /// The resultant . This item has not yet been added to the 's. - protected abstract void SelectItem(PlaylistItem item); + /// true if a selection occurred. + protected abstract bool SelectItem(PlaylistItem item); public override bool OnBackButton() { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index ff657e16fb..8a9c4db6ad 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } }, true); - Room.MaxAttempts.BindValueChanged(attempts => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true); + Room.MaxAttempts.BindValueChanged(_ => progressSection.Alpha = Room.MaxAttempts.Value != null ? 1 : 0, true); } protected override Drawable CreateMainContent() => new Container diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 65a27a9261..73765fc661 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists CreateNewItem = createNewItem }; - protected override void SelectItem(PlaylistItem item) + protected override bool SelectItem(PlaylistItem item) { switch (Playlist.Count) { @@ -39,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } this.Exit(); + return true; } private void createNewItem() diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index ca485aa8ea..c1223d7262 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -198,7 +198,7 @@ namespace osu.Game.Screens.Play dropOffScreen(obj, failTime, rotation, originalScale, originalPosition); // need to reapply the fail drop after judgement state changes - obj.ApplyCustomUpdateState += (o, _) => dropOffScreen(obj, failTime, rotation, originalScale, originalPosition); + obj.ApplyCustomUpdateState += (_, _) => dropOffScreen(obj, failTime, rotation, originalScale, originalPosition); appliedObjects.Add(obj); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6403943a53..9396b3311f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Timing; namespace osu.Game.Screens.Play @@ -101,6 +102,8 @@ namespace osu.Game.Screens.Play /// The destination time to seek to. public virtual void Seek(double time) { + Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); + AdjustableSource.Seek(time); // Manually process to make sure the gameplay clock is correctly updated after a seek. diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 99b9362700..c2652e9212 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Play }, }; - State.ValueChanged += s => InternalButtons.Deselect(); + State.ValueChanged += _ => InternalButtons.Deselect(); updateRetryCount(); } @@ -267,7 +267,7 @@ namespace osu.Game.Screens.Play { switch (e) { - case ScrollEvent _: + case ScrollEvent: if (ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) return globalAction.TriggerEvent(e); diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index aa9a2ab242..39a8f1e783 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Play.HUD overlayCircle.ScaleTo(0, 100) .Then().FadeOut().ScaleTo(1).FadeIn(500) - .OnComplete(a => + .OnComplete(_ => { icon.ScaleTo(1, 100); circularProgress.FadeOut(100).OnComplete(_ => diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index fe27e1fc24..8f80644d52 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -352,7 +352,7 @@ namespace osu.Game.Screens.Play // When the scoring mode changes, relative positions of elements may change (see DefaultSkin.GetDrawableComponent). // This is a best effort implementation for cases where users haven't customised layouts. scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); - scoringMode.BindValueChanged(val => Reload()); + scoringMode.BindValueChanged(_ => Reload()); } } } diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index ef28632d76..b6094726c0 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -157,10 +157,10 @@ namespace osu.Game.Screens.Play { switch (e) { - case KeyDownEvent _: - case KeyUpEvent _: - case MouseDownEvent _: - case MouseUpEvent _: + case KeyDownEvent: + case KeyUpEvent: + case MouseDownEvent: + case MouseUpEvent: return Target.Children.Any(c => c.TriggerEvent(e)); } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 0994a7d9bb..cd37c541ec 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -255,7 +255,7 @@ namespace osu.Game.Screens.Play ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; IClock IBeatSyncProvider.Clock => GameplayClock; - ChannelAmplitudes? IBeatSyncProvider.Amplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : (ChannelAmplitudes?)null; + ChannelAmplitudes? IBeatSyncProvider.Amplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : null; private class HardwareCorrectionOffsetClock : FramedOffsetClock { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4a8460ff46..4040adc48d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Play PrepareReplay(); - ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); + ScoreProcessor.NewJudgement += _ => ScoreProcessor.PopulateScore(Score.ScoreInfo); ScoreProcessor.OnResetFromReplayFrame += () => ScoreProcessor.PopulateScore(Score.ScoreInfo); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -315,7 +315,7 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Start(); }); - DrawableRuleset.IsPaused.BindValueChanged(paused => + DrawableRuleset.IsPaused.BindValueChanged(_ => { updateGameplayState(); updateSampleDisabledState(); diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 2a5e356df8..64b4853a67 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play protected override bool CheckModsAllowFailure() => false; public ReplayPlayer(Score score, PlayerConfiguration configuration = null) - : this((_, __) => score, configuration) + : this((_, _) => score, configuration) { } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index d5aec055fc..797787b194 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -45,6 +45,20 @@ namespace osu.Game.Screens.Play }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting => + { + if (GameplayClockContainer is MasterGameplayClockContainer master) + { + if (master.UserPlaybackRate.Value > 1 && waiting.NewValue) + master.UserPlaybackRate.Value = 1; + } + }, true); + } + protected override void StartGameplay() { base.StartGameplay(); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs index 5d7b04743e..3505786b64 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics Spacing = new Vector2(10, 0), Children = new[] { - base.CreateContent().With(d => + base.CreateContent().With(_ => { Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 384f4c4d4f..ab68dec92d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Ranking.Statistics Task.Run(() => { playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods); - }, loadCancellation.Token).ContinueWith(t => Schedule(() => + }, loadCancellation.Token).ContinueWith(_ => Schedule(() => { bool hitEventsAvailable = newScore.HitEvents.Count != 0; Container container; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 87a1fb4ccb..72e6c7d159 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -959,7 +959,7 @@ namespace osu.Game.Screens.Select { // root should always remain selected. if not, PerformSelection will not be called. State.Value = CarouselItemState.Selected; - State.ValueChanged += state => State.Value = CarouselItemState.Selected; + State.ValueChanged += _ => State.Value = CarouselItemState.Selected; this.carousel = carousel; } diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index dfcd43189b..bf6803f551 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select { switch (tab) { - case BeatmapDetailAreaDetailTabItem _: + case BeatmapDetailAreaDetailTabItem: Details.Show(); break; diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index fdc0d4efdd..2fe62c4a4e 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -217,7 +217,7 @@ namespace osu.Game.Screens.Select }); }; - lookup.Failure += e => + lookup.Failure += _ => { Schedule(() => { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1bea689e7c..1b3cab20e8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -53,7 +53,9 @@ namespace osu.Game.Screens.Select.Carousel private Action hideRequested; private Triangles triangles; + private StarCounter starCounter; + private DifficultyIcon difficultyIcon; [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } @@ -113,8 +115,9 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft, Children = new Drawable[] { - new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) + difficultyIcon = new DifficultyIcon(beatmapInfo) { + ShowTooltip = false, Scale = new Vector2(1.8f), }, new FillFlowContainer @@ -216,6 +219,8 @@ namespace osu.Game.Screens.Select.Carousel starDifficultyBindable.BindValueChanged(d => { starCounter.Current = (float)(d.NewValue?.Stars ?? 0); + if (d.NewValue != null) + difficultyIcon.Current.Value = d.NewValue.Value; }, true); } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index b0841caa47..cc904fc1da 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.BeatmapInfo, performBackgroundDifficultyLookup: false) + : base(item.BeatmapInfo) { filtered.BindTo(item.Filtered); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/GroupedDifficultyIcon.cs similarity index 53% rename from osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs rename to osu.Game/Screens/Select/Carousel/GroupedDifficultyIcon.cs index f883740fd7..8b4140df56 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/GroupedDifficultyIcon.cs @@ -8,23 +8,42 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { - public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon + /// + /// A difficulty icon that contains a counter on the right-side of it. + /// + /// + /// Used in cases when there are too many difficulty icons to show. + /// + public class GroupedDifficultyIcon : DifficultyIcon { public readonly List Items; - public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) - : base(items.Select(i => i.BeatmapInfo).ToList(), ruleset, Color4.White) + public GroupedDifficultyIcon(List items, RulesetInfo ruleset) + : base(items.OrderBy(b => b.BeatmapInfo.StarRating).Last().BeatmapInfo, ruleset) { Items = items; foreach (var item in items) item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); + AddInternal(new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Padding = new MarginPadding { Left = Size.X }, + Margin = new MarginPadding { Left = 2, Right = 5 }, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Text = items.Count.ToString(), + Colour = Color4.White, + }); + updateFilteredDisplay(); } diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 73a835d67f..577b3f5f64 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -92,8 +92,8 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) - .Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) + ? beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) + .Select(group => new GroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index a948e5b0bd..cc01f61c57 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName) .OrderByDescending(s => s.TotalScore), - (items, changes, ___) => + (items, _, _) => { Rank = items.FirstOrDefault()?.Rank; // Required since presence is changed via IsPresent override diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 59eb3fa607..693f182065 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.Select.Details modSettingChangeTracker?.Dispose(); modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - modSettingChangeTracker.SettingChanged += m => + modSettingChangeTracker.SettingChanged += _ => { debouncedStatisticsUpdate?.Cancel(); debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 92f707ae3e..03b72bf5e9 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Select { foreach (Match match in query_syntax_regex.Matches(query)) { - string key = match.Groups["key"].Value.ToLower(); + string key = match.Groups["key"].Value.ToLowerInvariant(); var op = parseOperator(match.Groups["op"].Value); string value = match.Groups["value"].Value; diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index 4d3c844753..a19acddb48 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Select { switch (item) { - case BeatmapDetailAreaDetailTabItem _: + case BeatmapDetailAreaDetailTabItem: return TabType.Details; case BeatmapDetailAreaLeaderboardTabItem leaderboardTab: diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a17e7823a6..7abcbfca42 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -360,7 +360,10 @@ namespace osu.Game.Screens.Select { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) + { + Logger.Log($"{nameof(FinaliseSelection)} aborted as carousel beatmaps are not yet loaded"); return; + } if (ruleset != null) Ruleset.Value = ruleset; @@ -494,7 +497,7 @@ namespace osu.Game.Screens.Select // clear pending task immediately to track any potential nested debounce operation. selectionChangedDebounce = null; - Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ShortName ?? "null"}"); + Logger.Log($"Song select updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ShortName ?? "null"}"); if (transferRulesetValue()) { @@ -519,7 +522,7 @@ namespace osu.Game.Screens.Select // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. if (!EqualityComparer.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo)) { - Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\""); + Logger.Log($"Song select changing beatmap from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap?.ToString() ?? "null"}\""); Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); } @@ -737,7 +740,10 @@ namespace osu.Game.Screens.Select bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack)) + { + Logger.Log($"Song select decided to {nameof(ensurePlayingSelected)}"); music.Play(true); + } lastTrack.SetTarget(track); } @@ -786,7 +792,17 @@ namespace osu.Game.Screens.Select Ruleset.ValueChanged += r => updateSelectedRuleset(r.NewValue); - decoupledRuleset.ValueChanged += r => Ruleset.Value = r.NewValue; + decoupledRuleset.ValueChanged += r => + { + bool wasDisabled = Ruleset.Disabled; + + // a sub-screen may have taken a lease on this decoupled ruleset bindable, + // which would indirectly propagate to the game-global bindable via the `DisabledChanged` callback below. + // to make sure changes sync without crashes, lift the disable for a short while to sync, and then restore the old value. + Ruleset.Disabled = false; + Ruleset.Value = r.NewValue; + Ruleset.Disabled = wasDisabled; + }; decoupledRuleset.DisabledChanged += r => Ruleset.Disabled = r; Beatmap.BindValueChanged(workingBeatmapChanged); diff --git a/osu.Game/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs index 61a97d92b0..b7d45ba642 100644 --- a/osu.Game/Screens/Utility/LatencyArea.cs +++ b/osu.Game/Screens/Utility/LatencyArea.cs @@ -6,8 +6,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics.Cursor; using osu.Game.Overlays; using osu.Game.Screens.Utility.SampleComponents; using osuTK.Input; @@ -15,7 +17,7 @@ using osuTK.Input; namespace osu.Game.Screens.Utility { [Cached] - public class LatencyArea : CompositeDrawable + public class LatencyArea : CompositeDrawable, IProvideCursor { [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } = null!; @@ -34,6 +36,10 @@ namespace osu.Game.Screens.Utility public readonly Bindable VisualMode = new Bindable(); + public CursorContainer? Cursor { get; private set; } + + public bool ProvidingUserCursor => IsActiveArea.Value; + public LatencyArea(Key key, int? targetFrameRate) { this.key = key; @@ -85,7 +91,7 @@ namespace osu.Game.Screens.Utility { RelativeSizeAxes = Axes.Both, }, - new LatencyCursorContainer + Cursor = new LatencyCursorContainer { RelativeSizeAxes = Axes.Both, }, @@ -99,7 +105,7 @@ namespace osu.Game.Screens.Utility { RelativeSizeAxes = Axes.Both, }, - new LatencyCursorContainer + Cursor = new LatencyCursorContainer { RelativeSizeAxes = Axes.Both, }, @@ -113,7 +119,7 @@ namespace osu.Game.Screens.Utility { RelativeSizeAxes = Axes.Both, }, - new LatencyCursorContainer + Cursor = new LatencyCursorContainer { RelativeSizeAxes = Axes.Both, }, diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs index 8675078a5e..c9d4dc7811 100644 --- a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs +++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs @@ -39,8 +39,6 @@ namespace osu.Game.Screens.Utility public override bool HideOverlaysOnEnter => true; - public override bool CursorVisible => mainArea.Count == 0; - public override float BackgroundParallaxAmount => 0; private readonly LinkFlowContainer explanatoryText; @@ -450,14 +448,14 @@ namespace osu.Game.Screens.Utility mainArea.AddRange(new[] { - new LatencyArea(Key.Number1, betterSide == 1 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) + new LatencyArea(Key.Number1, betterSide == 1 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : null) { Width = 0.5f, VisualMode = { BindTarget = VisualMode }, IsActiveArea = { Value = true }, ReportUserBest = () => recordResult(betterSide == 0), }, - new LatencyArea(Key.Number2, betterSide == 0 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) + new LatencyArea(Key.Number2, betterSide == 0 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : null) { Width = 0.5f, VisualMode = { BindTarget = VisualMode }, diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs index 40911684f8..aaf837eb60 100644 --- a/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs @@ -1,55 +1,50 @@ // 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.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Input.States; -using osu.Game.Overlays; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Utility.SampleComponents { - public class LatencyCursorContainer : LatencySampleComponent + public class LatencyCursorContainer : CursorContainer { - private Circle cursor = null!; + protected override Drawable CreateCursor() => new LatencyCursor(); - [Resolved] - private OverlayColourProvider overlayColourProvider { get; set; } = null!; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; public LatencyCursorContainer() { - Masking = true; + State.Value = Visibility.Hidden; } - protected override void LoadComplete() + protected override bool OnMouseMove(MouseMoveEvent e) { - base.LoadComplete(); - - InternalChild = cursor = new Circle - { - Size = new Vector2(40), - Origin = Anchor.Centre, - Colour = overlayColourProvider.Colour2, - }; + // Scheduling is required to ensure updating of cursor position happens in limited rate. + // We can alternatively solve this by a PassThroughInputManager layer inside LatencyArea, + // but that would mean including input lag to this test, which may not be desired. + Schedule(() => base.OnMouseMove(e)); + return false; } - protected override bool OnHover(HoverEvent e) => false; - - protected override void UpdateAtLimitedRate(InputState inputState) + private class LatencyCursor : LatencySampleComponent { - cursor.Colour = inputState.Mouse.IsPressed(MouseButton.Left) ? overlayColourProvider.Content1 : overlayColourProvider.Colour2; - - if (IsActive.Value) + public LatencyCursor() { - cursor.Position = ToLocalSpace(inputState.Mouse.Position); - cursor.Alpha = 1; + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChild = new Circle { Size = new Vector2(40) }; } - else + + protected override void UpdateAtLimitedRate(InputState inputState) { - cursor.Alpha = 0; + Colour = inputState.Mouse.IsPressed(MouseButton.Left) ? OverlayColourProvider.Content1 : OverlayColourProvider.Colour2; } } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 43b3cb0aa4..2467b7d039 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -74,7 +74,7 @@ namespace osu.Game.Skinning switch (target.Target) { case SkinnableTarget.SongSelect: - var songSelectComponents = new SkinnableTargetComponentsContainer(container => + var songSelectComponents = new SkinnableTargetComponentsContainer(_ => { // do stuff when we need to. }); diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs index 7de9cd5108..344a659627 100644 --- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs +++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs @@ -88,7 +88,7 @@ namespace osu.Game.Skinning.Editor } } - private class ToolboxComponentButton : OsuButton + public class ToolboxComponentButton : OsuButton { protected override bool ShouldBeConsideredForInput(Drawable child) => false; diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index f41cd63f2d..649b63dda4 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -190,13 +190,13 @@ namespace osu.Game.Skinning.Editor // schedule ensures this only happens when the skin editor is visible. // also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types). // probably something which will be factored out in a future database refactor so not too concerning for now. - currentSkin.BindValueChanged(skin => + currentSkin.BindValueChanged(_ => { hasBegunMutating = false; Scheduler.AddOnce(skinChanged); }, true); - SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true); + SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true); } public void UpdateTargetScreen(Drawable targetScreen) @@ -322,6 +322,12 @@ namespace osu.Game.Skinning.Editor protected override bool OnMouseDown(MouseDownEvent e) => true; + public override void Hide() + { + base.Hide(); + SelectedComponents.Clear(); + } + protected override void PopIn() { this diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index de33d49941..000917f728 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Skinning.Editor var editor = new SkinEditor(); - editor.State.BindValueChanged(visibility => updateComponentVisibility()); + editor.State.BindValueChanged(_ => updateComponentVisibility()); skinEditor = editor; diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 2aaf0ba18e..49914c53aa 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -120,16 +120,16 @@ namespace osu.Game.Skinning currentConfig.MinimumColumnWidth = minWidth; break; - case string _ when pair.Key.StartsWith("Colour", StringComparison.Ordinal): + case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal): HandleColours(currentConfig, line); break; // Custom sprite paths - case string _ when pair.Key.StartsWith("NoteImage", StringComparison.Ordinal): - case string _ when pair.Key.StartsWith("KeyImage", StringComparison.Ordinal): - case string _ when pair.Key.StartsWith("Hit", StringComparison.Ordinal): - case string _ when pair.Key.StartsWith("Stage", StringComparison.Ordinal): - case string _ when pair.Key.StartsWith("Lighting", StringComparison.Ordinal): + case string when pair.Key.StartsWith("NoteImage", StringComparison.Ordinal): + case string when pair.Key.StartsWith("KeyImage", StringComparison.Ordinal): + case string when pair.Key.StartsWith("Hit", StringComparison.Ordinal): + case string when pair.Key.StartsWith("Stage", StringComparison.Ordinal): + case string when pair.Key.StartsWith("Lighting", StringComparison.Ordinal): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs index 1b5de9abb5..ed31a8c3f5 100644 --- a/osu.Game/Skinning/SkinImporter.cs +++ b/osu.Game/Skinning/SkinImporter.cs @@ -214,7 +214,7 @@ namespace osu.Game.Skinning { try { - realm.Write(r => skin.Hash = ComputeHash(skin)); + realm.Write(_ => skin.Hash = ComputeHash(skin)); } catch (Exception e) { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index be6528db3f..dc0197e613 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -24,7 +24,6 @@ using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO; -using osu.Game.IO.Archives; using osu.Game.Overlays.Notifications; using osu.Game.Utils; @@ -168,7 +167,7 @@ namespace osu.Game.Skinning Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; - var result = skinImporter.Import(skinInfo); + var result = skinImporter.ImportModel(skinInfo); if (result != null) { @@ -260,9 +259,9 @@ namespace osu.Game.Skinning #region Implementation of IModelImporter - public Action>> PostImport + public Action>> PresentImport { - set => skinImporter.PostImport = value; + set => skinImporter.PresentImport = value; } public Task Import(params string[] paths) => skinImporter.Import(paths); @@ -275,9 +274,6 @@ namespace osu.Game.Skinning public Task> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken); - public Task> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) => - skinImporter.Import(archive, batchImport, cancellationToken); - #endregion public void Delete([CanBeNull] Expression> filter = null, bool silent = false) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index d7399b0141..e447570931 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -269,9 +269,9 @@ namespace osu.Game.Skinning { switch (lookup) { - case GlobalSkinColours _: - case SkinComboColourLookup _: - case SkinCustomColourLookup _: + case GlobalSkinColours: + case SkinComboColourLookup: + case SkinCustomColourLookup: if (provider.AllowColourLookup) return skin.GetConfig(lookup); diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 20232012f6..6fc9f60177 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -56,7 +56,7 @@ namespace osu.Game.Storyboards { var first = Alpha.Commands.FirstOrDefault(); - return first?.StartValue == 0 ? first.StartTime : (double?)null; + return first?.StartValue == 0 ? first.StartTime : null; } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 838dc618f2..a759482b5d 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -109,20 +109,20 @@ namespace osu.Game.Storyboards generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), + generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, _) => d.TransformBlendingMode(value, duration), false); if (drawable is IVectorScalable vectorScalable) { - generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (d, value) => vectorScalable.VectorScale = value, - (d, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); + generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value, + (_, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); } if (drawable is IFlippable flippable) { - generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), + generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, (_, value, duration, _) => flippable.TransformFlipH(value, duration), false); - generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), + generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, (_, value, duration, _) => flippable.TransformFlipV(value, duration), false); } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 3f5fee5768..31036247ab 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -17,9 +17,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Menu; using osu.Game.Skinning; @@ -58,15 +56,13 @@ namespace osu.Game.Tests.Visual Dependencies.CacheAs(testBeatmapManager = new TestBeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); } - protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; - public override void SetUpSteps() { base.SetUpSteps(); AddStep("load editor", LoadEditor); - AddUntilStep("wait for editor to load", () => EditorComponentsReady); + AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true); + AddUntilStep("wait for beatmap updated", () => !Beatmap.IsDefault); } protected virtual void LoadEditor() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index e5a1236abf..6b7495762a 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -87,8 +87,6 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (Room?.MatchState) { case TeamVersusRoomState teamVersus: - Debug.Assert(Room != null); - // simulate the server's automatic assignment of users to teams on join. // the "best" team is the one with the least users on it. int bestTeam = teamVersus.Teams @@ -103,6 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void RemoveUser(APIUser user) { Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserLeft(new MultiplayerRoomUser(user.Id)); Schedule(() => @@ -114,25 +113,23 @@ namespace osu.Game.Tests.Visual.Multiplayer public void ChangeRoomState(MultiplayerRoomState newState) { - Debug.Assert(Room != null); ((IMultiplayerClient)this).RoomStateChanged(newState); } public void ChangeUserState(int userId, MultiplayerUserState newState) { - Debug.Assert(Room != null); - ((IMultiplayerClient)this).UserStateChanged(userId, newState); updateRoomStateIfRequired(); } private void updateRoomStateIfRequired() { - Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Schedule(() => { + Debug.Assert(Room != null); + switch (Room.State) { case MultiplayerRoomState.Open: @@ -179,8 +176,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability) { - Debug.Assert(Room != null); - ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability); } @@ -272,8 +267,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task ChangeState(MultiplayerUserState newState) { - Debug.Assert(Room != null); - if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad) return Task.CompletedTask; @@ -292,7 +285,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public void ChangeUserMods(int userId, IEnumerable newMods) { - Debug.Assert(Room != null); ((IMultiplayerClient)this).UserModsChanged(userId, newMods.ToList()); } @@ -340,7 +332,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public override Task AbortGameplay() { - Debug.Assert(Room != null); Debug.Assert(LocalUser != null); ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle); diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 282312c9c1..6577057c17 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -4,14 +4,14 @@ #nullable disable using System; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Database; +using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; @@ -72,22 +72,18 @@ namespace osu.Game.Tests.Visual.OnlinePlay ((DummyAPIAccess)API).HandleRequest = request => { - TaskCompletionSource tcs = new TaskCompletionSource(); - - // Because some of the handlers use realm, we need to ensure the game is still alive when firing. - // If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late. - Scheduler.Add(() => + try { - bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); - tcs.SetResult(result); - }, false); - -#pragma warning disable RS0030 - // We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to - // the task being a TaskCompletionSource. - // Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument). - return tcs.Task.Result; -#pragma warning restore RS0030 + return handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); + } + catch (ObjectDisposedException) + { + // These requests can be fired asynchronously, but potentially arrive after game components + // have been disposed (ie. realm in BeatmapManager). + // This only happens in tests and it's easiest to ignore them for now. + Logger.Log($"Handled {nameof(ObjectDisposedException)} in test request handling"); + return true; + } }; }); diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index df0bdb0a50..b9f6183869 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual } [TearDownSteps] - public void TearDownSteps() + public virtual void TearDownSteps() { if (DebugUtils.IsNUnitRunning && Game != null) { diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 521fd8f21a..e1714299b6 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -39,10 +39,10 @@ namespace osu.Game.Tests.Visual base.SetUpSteps(); if (!HasCustomSteps) - CreateTest(null); + CreateTest(); } - protected void CreateTest(Action action) + protected void CreateTest([CanBeNull] Action action = null) { if (action != null && !HasCustomSteps) throw new InvalidOperationException($"Cannot add custom test steps without {nameof(HasCustomSteps)} being set."); diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 9adf6458cd..803db07fa0 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual } }); - Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); - Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index fc29c5aac5..2531f3c485 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Spectator FrameSendAttempts++; if (ShouldFailSendingFrames) - return Task.FromException(new InvalidOperationException()); + return Task.FromException(new InvalidOperationException($"Intentional fail via {nameof(ShouldFailSendingFrames)}")); return ((ISpectatorClient)this).UserSentFrames(api.LocalUser.Value.Id, bundle); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e9bf000109..ab690b22a6 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 dd344b0cd0..a84fc40a45 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ - - + + @@ -84,7 +84,7 @@ - + diff --git a/osu.iOS/IOSMouseSettings.cs b/osu.iOS/IOSMouseSettings.cs new file mode 100644 index 0000000000..1979a881f7 --- /dev/null +++ b/osu.iOS/IOSMouseSettings.cs @@ -0,0 +1,36 @@ +// 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.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; +using osu.Game.Overlays.Settings; + +namespace osu.iOS +{ + public class IOSMouseSettings : SettingsSubsection + { + protected override LocalisableString Header => MouseSettingsStrings.Mouse; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager osuConfig) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust, + TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip, + Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel), + }, + new SettingsCheckbox + { + LabelText = MouseSettingsStrings.DisableMouseButtons, + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons), + }, + }; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 053bce1618..452b573389 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -6,7 +6,10 @@ using System; using Foundation; using osu.Framework.Graphics; +using osu.Framework.Input.Handlers; +using osu.Framework.iOS.Input; using osu.Game; +using osu.Game.Overlays.Settings; using osu.Game.Updater; using osu.Game.Utils; using Xamarin.Essentials; @@ -26,6 +29,18 @@ namespace osu.iOS // Because we have the home indicator (mostly) hidden we don't really care about drawing in this region. Edges.Bottom; + public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler) + { + switch (handler) + { + case IOSMouseHandler _: + return new IOSMouseSettings(); + + default: + return base.CreateSettingsSubsectionFor(handler); + } + } + private class IOSBatteryInfo : BatteryInfo { public override double ChargeLevel => Battery.ChargeLevel; diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj index 1203c3659b..b9da874f30 100644 --- a/osu.iOS/osu.iOS.csproj +++ b/osu.iOS/osu.iOS.csproj @@ -23,6 +23,7 @@ + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 286a1eb29f..0794095854 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -7,11 +7,15 @@ ExplicitlyExcluded SOLUTION WARNING + WARNING + WARNING WARNING WARNING HINT HINT WARNING + WARNING + WARNING WARNING True WARNING @@ -59,13 +63,16 @@ HINT HINT WARNING + DO_NOT_SHOW WARNING WARNING WARNING WARNING + DO_NOT_SHOW WARNING WARNING WARNING + DO_NOT_SHOW WARNING WARNING WARNING @@ -91,6 +98,7 @@ WARNING HINT DO_NOT_SHOW + HINT HINT HINT ERROR @@ -118,7 +126,7 @@ WARNING WARNING WARNING - HINT + HINT WARNING WARNING WARNING @@ -126,6 +134,8 @@ HINT HINT WARNING + HINT + HINT HINT HINT HINT @@ -133,6 +143,8 @@ HINT HINT WARNING + HINT + HINT WARNING WARNING WARNING @@ -143,6 +155,7 @@ DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW + WARNING WARNING WARNING WARNING @@ -263,6 +276,7 @@ Explicit ExpressionBody BlockBody + ExplicitlyTyped True NEXT_LINE True @@ -791,7 +805,7 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> True - True + True True True True