From 1d314a1f4b86ce5fd6ea95804acb8d411d95413c Mon Sep 17 00:00:00 2001
From: voidedWarranties <wong.zhao@voidedxd.xyz>
Date: Wed, 11 Mar 2020 22:40:08 -0700
Subject: [PATCH 001/434] Prevent playback from going beyond song end

---
 .../Edit/Components/PlaybackControl.cs        |  3 +++
 osu.Game/Screens/Edit/EditorClock.cs          | 19 +++++++++++++++++++
 2 files changed, 22 insertions(+)

diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 897c6ec531..ff650a7ad7 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -87,6 +87,9 @@ namespace osu.Game.Screens.Edit.Components
 
         private void togglePause()
         {
+            if ((adjustableClock as EditorClock)?.PlaybackFinished == true)
+                adjustableClock.Seek(0);
+
             if (adjustableClock.IsRunning)
                 adjustableClock.Stop();
             else
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index e5e47507f3..0e5b42fe69 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit
 
         private readonly BindableBeatDivisor beatDivisor;
 
+        public bool PlaybackFinished { get; private set; }
+
         public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
         {
             this.beatDivisor = beatDivisor;
@@ -37,6 +39,23 @@ namespace osu.Game.Screens.Edit
             TrackLength = trackLength;
         }
 
+        public override void ProcessFrame()
+        {
+            base.ProcessFrame();
+
+            if (CurrentTime >= TrackLength)
+            {
+                if (!PlaybackFinished)
+                {
+                    PlaybackFinished = true;
+                    Stop();
+                    Seek(TrackLength);
+                }
+            }
+            else
+                PlaybackFinished = false;
+        }
+
         /// <summary>
         /// Seek to the closest snappable beat from a time.
         /// </summary>

From 7e4f58c2d3adc15ccb14a69d99094de5efcd7c13 Mon Sep 17 00:00:00 2001
From: voidedWarranties <wong.zhao@voidedxd.xyz>
Date: Fri, 13 Mar 2020 16:42:05 -0700
Subject: [PATCH 002/434] Internalize both looping and stopping

---
 .../Screens/Edit/Components/PlaybackControl.cs     |  3 ---
 osu.Game/Screens/Edit/EditorClock.cs               | 14 ++++++++------
 2 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index ff650a7ad7..897c6ec531 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -87,9 +87,6 @@ namespace osu.Game.Screens.Edit.Components
 
         private void togglePause()
         {
-            if ((adjustableClock as EditorClock)?.PlaybackFinished == true)
-                adjustableClock.Seek(0);
-
             if (adjustableClock.IsRunning)
                 adjustableClock.Stop();
             else
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 0e5b42fe69..aef304bd6e 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit
 
         private readonly BindableBeatDivisor beatDivisor;
 
-        public bool PlaybackFinished { get; private set; }
+        private bool playbackFinished;
 
         public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor)
         {
@@ -43,17 +43,19 @@ namespace osu.Game.Screens.Edit
         {
             base.ProcessFrame();
 
-            if (CurrentTime >= TrackLength)
+            var playbackAlreadyStopped = playbackFinished;
+            playbackFinished = CurrentTime >= TrackLength;
+
+            if (playbackFinished && IsRunning)
             {
-                if (!PlaybackFinished)
+                if (!playbackAlreadyStopped)
                 {
-                    PlaybackFinished = true;
                     Stop();
                     Seek(TrackLength);
                 }
+                else
+                    Seek(0);
             }
-            else
-                PlaybackFinished = false;
         }
 
         /// <summary>

From a38c912c6d4060a85f5172adf1bcc89e0ed9dc0e Mon Sep 17 00:00:00 2001
From: voidedWarranties <wong.zhao@voidedxd.xyz>
Date: Sat, 21 Mar 2020 12:15:20 -0700
Subject: [PATCH 003/434] Test stopping behavior

---
 .../Visual/Editor/TestSceneEditorClock.cs     | 51 +++++++++++++++++++
 .../Visual/Editor/TimelineTestScene.cs        | 16 +++---
 2 files changed, 58 insertions(+), 9 deletions(-)
 create mode 100644 osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs

diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs
new file mode 100644
index 0000000000..0b128a974c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Screens.Edit.Components;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Editor
+{
+    [TestFixture]
+    public class TestSceneEditorClock : EditorClockTestScene
+    {
+        public TestSceneEditorClock()
+        {
+            Add(new FillFlowContainer
+            {
+                RelativeSizeAxes = Axes.Both,
+                Children = new Drawable[]
+                {
+                    new TimeInfoContainer
+                    {
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.Centre,
+                        Size = new Vector2(200, 100)
+                    },
+                    new PlaybackControl
+                    {
+                        Anchor = Anchor.Centre,
+                        Origin = Anchor.Centre,
+                        Size = new Vector2(200, 100)
+                    }
+                }
+            });
+        }
+
+        [Test]
+        public void TestStopAtTrackEnd()
+        {
+            AddStep("Reset clock", () => Clock.Seek(0));
+            AddStep("Start clock", Clock.Start);
+            AddAssert("Clock running", () => Clock.IsRunning);
+            AddStep("Seek near end", () => Clock.Seek(Clock.TrackLength - 250));
+            AddUntilStep("Clock stops", () => !Clock.IsRunning);
+            AddAssert("Clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
+            AddStep("Start clock again", Clock.Start);
+            AddAssert("Clock looped", () => Clock.IsRunning && Clock.CurrentTime < Clock.TrackLength);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
index 7081eb3af5..83a0455b46 100644
--- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs
@@ -113,7 +113,6 @@ namespace osu.Game.Tests.Visual.Editor
         private class StartStopButton : OsuButton
         {
             private IAdjustableClock adjustableClock;
-            private bool started;
 
             public StartStopButton()
             {
@@ -132,18 +131,17 @@ namespace osu.Game.Tests.Visual.Editor
 
             private void onClick()
             {
-                if (started)
-                {
+                if (adjustableClock.IsRunning)
                     adjustableClock.Stop();
-                    Text = "Start";
-                }
                 else
-                {
                     adjustableClock.Start();
-                    Text = "Stop";
-                }
+            }
 
-                started = !started;
+            protected override void Update()
+            {
+                base.Update();
+
+                Text = adjustableClock.IsRunning ? "Stop" : "Start";
             }
         }
     }

From b41f3f1cad7312708d864269cfb1c587ed2353ca Mon Sep 17 00:00:00 2001
From: voidedWarranties <wong.zhao@voidedxd.xyz>
Date: Mon, 23 Mar 2020 22:37:53 -0700
Subject: [PATCH 004/434] Fix seeking back to beginning too early

---
 .../Compose/Components/Timeline/Timeline.cs   |  4 +++-
 osu.Game/Screens/Edit/EditorClock.cs          | 21 +++++++++++--------
 2 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index ddca5e42c2..590abf20b4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -130,7 +130,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
             if (!track.IsLoaded)
                 return;
 
-            adjustableClock.Seek(Current / Content.DrawWidth * track.Length);
+            double target = Current / Content.DrawWidth * track.Length;
+
+            adjustableClock.Seek(Math.Min(track.Length, target));
         }
 
         private void scrollToTrackTime()
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index aef304bd6e..b4ab867774 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -43,18 +43,21 @@ namespace osu.Game.Screens.Edit
         {
             base.ProcessFrame();
 
-            var playbackAlreadyStopped = playbackFinished;
-            playbackFinished = CurrentTime >= TrackLength;
-
-            if (playbackFinished && IsRunning)
+            if (IsRunning)
             {
-                if (!playbackAlreadyStopped)
+                var playbackAlreadyStopped = playbackFinished;
+                playbackFinished = CurrentTime >= TrackLength;
+
+                if (playbackFinished)
                 {
-                    Stop();
-                    Seek(TrackLength);
+                    if (!playbackAlreadyStopped)
+                    {
+                        Stop();
+                        Seek(TrackLength);
+                    }
+                    else
+                        Seek(0);
                 }
-                else
-                    Seek(0);
             }
         }
 

From d42c872f8f19923dd56388cd1ba632cc7298292a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 25 Mar 2020 16:02:20 +0900
Subject: [PATCH 005/434] Better ensure track restarted

---
 osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs
index 0b128a974c..a824696022 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorClock.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Editor
             AddUntilStep("Clock stops", () => !Clock.IsRunning);
             AddAssert("Clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
             AddStep("Start clock again", Clock.Start);
-            AddAssert("Clock looped", () => Clock.IsRunning && Clock.CurrentTime < Clock.TrackLength);
+            AddAssert("Clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
         }
     }
 }

From 00574a528868d1ef31907484e71623969c0cfeaf Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Feb 2021 18:32:28 +0900
Subject: [PATCH 006/434] Use ISample everywhere in Skin GetSample lookup path

---
 .../Skinning/Legacy/ManiaLegacySkinTransformer.cs           | 2 +-
 osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs         | 2 +-
 osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs       | 2 +-
 .../Skinning/Legacy/TaikoLegacySkinTransformer.cs           | 2 +-
 osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs   | 2 +-
 osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs       | 4 ++--
 .../NonVisual/Skinning/LegacySkinAnimationTest.cs           | 2 +-
 osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs    | 2 +-
 .../Visual/Gameplay/TestSceneSkinnableDrawable.cs           | 6 +++---
 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs   | 2 +-
 osu.Game/Skinning/DefaultSkin.cs                            | 2 +-
 osu.Game/Skinning/ISkin.cs                                  | 2 +-
 osu.Game/Skinning/LegacyBeatmapSkin.cs                      | 2 +-
 osu.Game/Skinning/LegacySkin.cs                             | 2 +-
 osu.Game/Skinning/LegacySkinTransformer.cs                  | 2 +-
 osu.Game/Skinning/Skin.cs                                   | 2 +-
 osu.Game/Skinning/SkinManager.cs                            | 2 +-
 osu.Game/Skinning/SkinProvidingContainer.cs                 | 4 ++--
 18 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index cbbbacfe19..24ccae895d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
             return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
         }
 
-        public override Sample GetSample(ISampleInfo sampleInfo)
+        public override ISample GetSample(ISampleInfo sampleInfo)
         {
             // layered hit sounds never play in mania
             if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index e2d9f144c0..8fd13c7417 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests
                 return null;
             }
 
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
 
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 8dbb48c048..19b6779619 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Tests
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
 
-            public Sample GetSample(ISampleInfo sampleInfo) => null;
+            public ISample GetSample(ISampleInfo sampleInfo) => null;
 
             public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 9f29675230..d1214d3456 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
             throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
         }
 
-        public override Sample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
+        public override ISample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
 
         public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup);
 
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index 3ded3009bd..883791c35c 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Gameplay
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
 
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
             {
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 7a0dd5b719..6fa1839556 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Gameplay
         public void TestRetrieveTopLevelSample()
         {
             ISkin skin = null;
-            Sample channel = null;
+            ISample channel = null;
 
             AddStep("create skin", () => skin = new TestSkin("test-sample", this));
             AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample")));
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Gameplay
         public void TestRetrieveSampleInSubFolder()
         {
             ISkin skin = null;
-            Sample channel = null;
+            ISample channel = null;
 
             AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this));
             AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample")));
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index da004b9088..b08a228de3 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
             }
 
             public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotSupportedException();
         }
 
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 414f7d3f88..732a3f3f42 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -219,7 +219,7 @@ namespace osu.Game.Tests.Skins
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
 
-            public Sample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
+            public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
         }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 44142b69d7..7a6e2f54c2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
 
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
         }
@@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
 
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
         }
@@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
 
-            public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
 
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
 
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index d688e9cb21..d792405eeb 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
             public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
-            public Sample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
+            public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
             public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
 
             public void TriggerSourceChanged()
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs
index 346c7b3c65..0b3f5f3cde 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/DefaultSkin.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Skinning
 
         public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
 
-        public override Sample GetSample(ISampleInfo sampleInfo) => null;
+        public override ISample GetSample(ISampleInfo sampleInfo) => null;
 
         public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
         {
diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs
index ef8de01042..73f7cf6d39 100644
--- a/osu.Game/Skinning/ISkin.cs
+++ b/osu.Game/Skinning/ISkin.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Skinning
         /// <param name="sampleInfo">The requested sample.</param>
         /// <returns>A matching sample channel, or null if unavailable.</returns>
         [CanBeNull]
-        Sample GetSample(ISampleInfo sampleInfo);
+        ISample GetSample(ISampleInfo sampleInfo);
 
         /// <summary>
         /// Retrieve a configuration value.
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index fb4207b647..3ec205e897 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Skinning
             return base.GetConfig<TLookup, TValue>(lookup);
         }
 
-        public override Sample GetSample(ISampleInfo sampleInfo)
+        public override ISample GetSample(ISampleInfo sampleInfo)
         {
             if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
             {
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index e5d0217671..1ee797098c 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -452,7 +452,7 @@ namespace osu.Game.Skinning
             return null;
         }
 
-        public override Sample GetSample(ISampleInfo sampleInfo)
+        public override ISample GetSample(ISampleInfo sampleInfo)
         {
             IEnumerable<string> lookupNames;
 
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index e2f4a82a54..ae8faf1a3b 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Skinning
         public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
             => Source.GetTexture(componentName, wrapModeS, wrapModeT);
 
-        public virtual Sample GetSample(ISampleInfo sampleInfo)
+        public virtual ISample GetSample(ISampleInfo sampleInfo)
         {
             if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
                 return Source.GetSample(sampleInfo);
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index e8d84b49f9..13f5385c20 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Skinning
 
         public abstract Drawable GetDrawableComponent(ISkinComponent componentName);
 
-        public abstract Sample GetSample(ISampleInfo sampleInfo);
+        public abstract ISample GetSample(ISampleInfo sampleInfo);
 
         public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
 
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 2826c826a5..9e730b2ce1 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -171,7 +171,7 @@ namespace osu.Game.Skinning
 
         public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT);
 
-        public Sample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
+        public ISample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
 
         public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => CurrentSkin.Value.GetConfig<TLookup, TValue>(lookup);
 
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index ba67d0a678..cf22b2e820 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -59,9 +59,9 @@ namespace osu.Game.Skinning
             return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
         }
 
-        public Sample GetSample(ISampleInfo sampleInfo)
+        public ISample GetSample(ISampleInfo sampleInfo)
         {
-            Sample sourceChannel;
+            ISample sourceChannel;
             if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
                 return sourceChannel;
 

From 4aff54412a0a23707b4284510705dd2b2c91492d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Feb 2021 18:32:39 +0900
Subject: [PATCH 007/434] Move dispose method to end of file

---
 osu.Game/Skinning/LegacySkin.cs | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 1ee797098c..12abc4d867 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -100,13 +100,6 @@ namespace osu.Game.Skinning
                 true) != null);
         }
 
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            Textures?.Dispose();
-            Samples?.Dispose();
-        }
-
         public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
         {
             switch (lookup)
@@ -504,5 +497,12 @@ namespace osu.Game.Skinning
             string lastPiece = componentName.Split('/').Last();
             yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
         }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            Textures?.Dispose();
+            Samples?.Dispose();
+        }
     }
 }

From 880fe820733d9159c6eb866f85336b01081ea1c2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Feb 2021 18:32:51 +0900
Subject: [PATCH 008/434] Add sample wrapper in LegacySkin to keep a reference
 and avoid GC death

---
 osu.Game/Skinning/LegacySkin.cs | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 12abc4d867..5d015ca5ab 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -461,12 +461,43 @@ namespace osu.Game.Skinning
                 var sample = Samples?.Get(lookup);
 
                 if (sample != null)
-                    return sample;
+                    return new LegacySkinSample(sample, this);
             }
 
             return null;
         }
 
+        /// <summary>
+        /// A sample wrapper which keeps a reference to the contained skin to avoid finalizer garbage collection of the managing SampleStore.
+        /// </summary>
+        private class LegacySkinSample : ISample
+        {
+            private readonly Sample sample;
+
+            [UsedImplicitly]
+            private readonly LegacySkin skin;
+
+            public LegacySkinSample(Sample sample, LegacySkin skin)
+            {
+                this.sample = sample;
+                this.skin = skin;
+            }
+
+            public SampleChannel Play()
+            {
+                return sample.Play();
+            }
+
+            public SampleChannel GetChannel()
+            {
+                return sample.GetChannel();
+            }
+
+            public double Length => sample.Length;
+
+            public Bindable<int> PlaybackConcurrency => sample.PlaybackConcurrency;
+        }
+
         private IEnumerable<string> getLegacyLookupNames(HitSampleInfo hitSample)
         {
             var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames);

From 487a39eea95a5215d5aec323c923a600144ba51a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Feb 2021 18:52:34 +0900
Subject: [PATCH 009/434] Update interface implementations with framework
 changes

---
 .../TestSceneDrawableRulesetDependencies.cs   |  6 ++--
 .../UI/DrawableRulesetDependencies.cs         | 11 +++++++
 osu.Game/Skinning/LegacySkin.cs               | 33 +++++++++++++++++++
 osu.Game/Skinning/PoolableSkinnableSample.cs  |  8 +++++
 osu.Game/Skinning/SkinnableSound.cs           | 17 ++++++----
 5 files changed, 67 insertions(+), 8 deletions(-)

diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index 787f72ba79..a2f2c5e41f 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -118,9 +118,11 @@ namespace osu.Game.Tests.Rulesets
             public BindableNumber<double> Frequency => throw new NotImplementedException();
             public BindableNumber<double> Tempo => throw new NotImplementedException();
 
-            public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
+            public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+            public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
 
-            public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
+            public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
+            public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
 
             public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException();
 
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index deec948d14..6c31f05337 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -114,6 +114,11 @@ namespace osu.Game.Rulesets.UI
 
             public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotSupportedException();
 
+            public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
+            {
+                throw new NotImplementedException();
+            }
+
             public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
 
             public BindableNumber<double> Volume => throw new NotSupportedException();
@@ -124,6 +129,12 @@ namespace osu.Game.Rulesets.UI
 
             public BindableNumber<double> Tempo => throw new NotSupportedException();
 
+            public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
+            public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
+            public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
+
             public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
 
             public IBindable<double> AggregateVolume => throw new NotSupportedException();
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 5d015ca5ab..2edc36a770 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -7,6 +7,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using JetBrains.Annotations;
+using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -496,6 +497,38 @@ namespace osu.Game.Skinning
             public double Length => sample.Length;
 
             public Bindable<int> PlaybackConcurrency => sample.PlaybackConcurrency;
+            public BindableNumber<double> Volume => sample.Volume;
+
+            public BindableNumber<double> Balance => sample.Balance;
+
+            public BindableNumber<double> Frequency => sample.Frequency;
+
+            public BindableNumber<double> Tempo => sample.Tempo;
+
+            public void BindAdjustments(IAggregateAudioAdjustment component)
+            {
+                sample.BindAdjustments(component);
+            }
+
+            public void UnbindAdjustments(IAggregateAudioAdjustment component)
+            {
+                sample.UnbindAdjustments(component);
+            }
+
+            public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
+            {
+                sample.AddAdjustment(type, adjustBindable);
+            }
+
+            public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
+            {
+                sample.RemoveAdjustment(type, adjustBindable);
+            }
+
+            public void RemoveAllAdjustments(AdjustableProperty type)
+            {
+                sample.RemoveAllAdjustments(type);
+            }
         }
 
         private IEnumerable<string> getLegacyLookupNames(HitSampleInfo hitSample)
diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 9025fdbd0f..b12fbf90f3 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -165,6 +165,14 @@ namespace osu.Game.Skinning
 
         public BindableNumber<double> Tempo => sampleContainer.Tempo;
 
+        public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component);
+
+        public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component);
+
+        public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
+
+        public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
+
         public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
 
         public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index b3db2d6558..c971517c7f 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -176,14 +176,19 @@ namespace osu.Game.Skinning
 
         public BindableNumber<double> Tempo => samplesContainer.Tempo;
 
-        public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
-            => samplesContainer.AddAdjustment(type, adjustBindable);
+        public void BindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.BindAdjustments(component);
 
-        public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable)
-            => samplesContainer.RemoveAdjustment(type, adjustBindable);
+        public void UnbindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.UnbindAdjustments(component);
 
-        public void RemoveAllAdjustments(AdjustableProperty type)
-            => samplesContainer.RemoveAllAdjustments(type);
+        public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable);
+
+        public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable);
+
+        public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable);
+
+        public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable);
+
+        public void RemoveAllAdjustments(AdjustableProperty type) => samplesContainer.RemoveAllAdjustments(type);
 
         /// <summary>
         /// Whether any samples are currently playing.

From 772471a6d826a730ca17176f2c542b7e4d2ba44a Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 09:34:39 +0300
Subject: [PATCH 010/434] Add failing test case

---
 .../Gameplay/TestScenePauseWhenInactive.cs    | 63 ++++++++++++++++---
 osu.Game/Screens/Play/Player.cs               | 10 +--
 2 files changed, 58 insertions(+), 15 deletions(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
index e43e5ba3ce..15412fea00 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -1,28 +1,28 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Collections.Generic;
+using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
+using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osuTK.Input;
 
 namespace osu.Game.Tests.Visual.Gameplay
 {
     [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state.
     public class TestScenePauseWhenInactive : OsuPlayerTestScene
     {
-        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
-        {
-            var beatmap = (Beatmap)base.CreateBeatmap(ruleset);
-
-            beatmap.HitObjects.RemoveAll(h => h.StartTime < 30000);
-
-            return beatmap;
-        }
-
         [Resolved]
         private GameHost host { get; set; }
 
@@ -33,10 +33,53 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             AddStep("resume player", () => Player.GameplayClockContainer.Start());
             AddAssert("ensure not paused", () => !Player.GameplayClockContainer.IsPaused.Value);
+
+            AddStep("progress time to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime));
+            AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value);
+        }
+
+        /// <summary>
+        /// Tests that if a pause from focus lose is performed while in pause cooldown,
+        /// the player will still pause after the cooldown is finished.
+        /// </summary>
+        [Test]
+        public void TestPauseWhileInCooldown()
+        {
+            AddStep("resume player", () => Player.GameplayClockContainer.Start());
+            AddStep("skip to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime));
+
+            AddStep("set inactive", () => ((Bindable<bool>)host.IsActive).Value = false);
+            AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value);
+
+            AddStep("set active", () => ((Bindable<bool>)host.IsActive).Value = true);
+
+            AddStep("resume player", () => Player.Resume());
+            AddStep("click resume overlay", () =>
+            {
+                InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single());
+                InputManager.Click(MouseButton.Left);
+            });
+
+            AddAssert("pause cooldown active", () => Player.PauseCooldownActive);
+            AddStep("set inactive again", () => ((Bindable<bool>)host.IsActive).Value = false);
             AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value);
-            AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime);
         }
 
         protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true);
+
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+        {
+            return new Beatmap
+            {
+                HitObjects = new List<HitObject>
+                {
+                    new HitCircle { StartTime = 30000 },
+                    new HitCircle { StartTime = 35000 },
+                },
+            };
+        }
+
+        protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+            => new TestWorkingBeatmap(beatmap, storyboard, Audio);
     }
 }
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 74059da21a..a7acda926b 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -667,6 +667,9 @@ namespace osu.Game.Screens.Play
 
         private double? lastPauseActionTime;
 
+        public bool PauseCooldownActive =>
+            lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
+
         private bool canPause =>
             // must pass basic screen conditions (beatmap loaded, instance allows pause)
             LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume
@@ -675,10 +678,7 @@ namespace osu.Game.Screens.Play
             // cannot pause if we are already in a fail state
             && !HasFailed
             // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state.
-            && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive));
-
-        private bool pauseCooldownActive =>
-            lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
+            && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !PauseCooldownActive));
 
         private bool canResume =>
             // cannot resume from a non-paused state
@@ -812,7 +812,7 @@ namespace osu.Game.Screens.Play
             // ValidForResume is false when restarting
             if (ValidForResume)
             {
-                if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
+                if (PauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
                     // still want to block if we are within the cooldown period and not already paused.
                     return true;
             }

From 4436585aa4dea5bec025e5bf816ed23008b4c87e Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 09:35:29 +0300
Subject: [PATCH 011/434] Keep attempting to pause gameplay while window not
 active

---
 osu.Game/Screens/Play/Player.cs | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index a7acda926b..72d9a60c91 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -427,11 +427,16 @@ namespace osu.Game.Screens.Play
 
         private void updatePauseOnFocusLostState()
         {
-            if (!PauseOnFocusLost || breakTracker.IsBreakTime.Value)
+            if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value)
                 return;
 
             if (gameActive.Value == false)
-                Pause();
+            {
+                if (canPause)
+                    Pause();
+                else
+                    Scheduler.AddDelayed(updatePauseOnFocusLostState, 200);
+            }
         }
 
         private IBeatmap loadPlayableBeatmap()

From ddd1dcff88428460cfcde74c963196a0518924fe Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 11:33:26 +0300
Subject: [PATCH 012/434] Attempt pausing every single frame

---
 osu.Game/Screens/Play/Player.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 72d9a60c91..fa545859d4 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -435,7 +435,7 @@ namespace osu.Game.Screens.Play
                 if (canPause)
                     Pause();
                 else
-                    Scheduler.AddDelayed(updatePauseOnFocusLostState, 200);
+                    Scheduler.AddOnce(updatePauseOnFocusLostState);
             }
         }
 

From 0771154dd2831f4d7de9d28913965f49df8909ce Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 11:42:30 +0300
Subject: [PATCH 013/434] Make `PauseCooldownActive` protected and expose on
 test class

---
 osu.Game/Screens/Play/Player.cs     | 2 +-
 osu.Game/Tests/Visual/TestPlayer.cs | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index fa545859d4..8c816e8030 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -672,7 +672,7 @@ namespace osu.Game.Screens.Play
 
         private double? lastPauseActionTime;
 
-        public bool PauseCooldownActive =>
+        protected bool PauseCooldownActive =>
             lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown;
 
         private bool canPause =>
diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs
index f47391ce6a..0addc9de75 100644
--- a/osu.Game/Tests/Visual/TestPlayer.cs
+++ b/osu.Game/Tests/Visual/TestPlayer.cs
@@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual
 
         public new HealthProcessor HealthProcessor => base.HealthProcessor;
 
+        public new bool PauseCooldownActive => base.PauseCooldownActive;
+
         public readonly List<JudgementResult> Results = new List<JudgementResult>();
 
         public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)

From fe5e45ea8180931f1c4c8d02a162e50b0000f186 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 11:43:33 +0300
Subject: [PATCH 014/434] Move gameplay cursor outside instead and fix
 potential failure

---
 .../Gameplay/TestScenePauseWhenInactive.cs    | 22 +++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
index 15412fea00..fa596c4823 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -2,21 +2,18 @@
 // 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.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
-using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Storyboards;
 using osu.Game.Tests.Beatmaps;
-using osuTK.Input;
+using osuTK;
 
 namespace osu.Game.Tests.Visual.Gameplay
 {
@@ -45,6 +42,8 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestPauseWhileInCooldown()
         {
+            AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
+
             AddStep("resume player", () => Player.GameplayClockContainer.Start());
             AddStep("skip to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime));
 
@@ -54,14 +53,15 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("set active", () => ((Bindable<bool>)host.IsActive).Value = true);
 
             AddStep("resume player", () => Player.Resume());
-            AddStep("click resume overlay", () =>
-            {
-                InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single());
-                InputManager.Click(MouseButton.Left);
-            });
 
-            AddAssert("pause cooldown active", () => Player.PauseCooldownActive);
-            AddStep("set inactive again", () => ((Bindable<bool>)host.IsActive).Value = false);
+            bool pauseCooldownActive = false;
+
+            AddStep("set inactive again", () =>
+            {
+                pauseCooldownActive = Player.PauseCooldownActive;
+                ((Bindable<bool>)host.IsActive).Value = false;
+            });
+            AddAssert("pause cooldown active", () => pauseCooldownActive);
             AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value);
         }
 

From f6c279ab00d6af3231e09332e4d35b4c2ce7e106 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 19 Feb 2021 11:45:45 +0300
Subject: [PATCH 015/434] Add assert ensuring player resumed properly

---
 osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
index fa596c4823..49c1163c6c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("set active", () => ((Bindable<bool>)host.IsActive).Value = true);
 
             AddStep("resume player", () => Player.Resume());
+            AddAssert("unpaused", () => !Player.GameplayClockContainer.IsPaused.Value);
 
             bool pauseCooldownActive = false;
 

From 362e4802f761980213893e30c2de0c038b1463db Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Feb 2021 17:58:04 +0900
Subject: [PATCH 016/434] Add the ability for PerformFromMenuRunner to inspect
 nested screen stacks

---
 osu.Game/PerformFromMenuRunner.cs             | 21 ++++++++++++++++---
 osu.Game/Screens/IHasSubScreenStack.cs        | 15 +++++++++++++
 .../Screens/OnlinePlay/OnlinePlayScreen.cs    |  4 +++-
 3 files changed, 36 insertions(+), 4 deletions(-)
 create mode 100644 osu.Game/Screens/IHasSubScreenStack.cs

diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index a4179c94da..39889ea7fc 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Dialog;
 using osu.Game.Overlays.Notifications;
+using osu.Game.Screens;
 using osu.Game.Screens.Menu;
 
 namespace osu.Game
@@ -81,27 +82,41 @@ namespace osu.Game
 
             game?.CloseAllOverlays(false);
 
-            // we may already be at the target screen type.
+            findValidTarget(current);
+        }
+
+        private bool findValidTarget(IScreen current)
+        {
             var type = current.GetType();
 
+            // check if we are already at a valid target screen.
             if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
             {
                 finalAction(current);
                 Cancel();
-                return;
+                return true;
             }
 
             while (current != null)
             {
+                // if this has a sub stack, recursively check the screens within it.
+                if (current is IHasSubScreenStack currentSubScreen)
+                {
+                    if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen))
+                        return true;
+                }
+
                 if (validScreens.Any(t => t.IsAssignableFrom(type)))
                 {
                     current.MakeCurrent();
-                    break;
+                    return true;
                 }
 
                 current = current.GetParentScreen();
                 type = current?.GetType();
             }
+
+            return false;
         }
 
         /// <summary>
diff --git a/osu.Game/Screens/IHasSubScreenStack.cs b/osu.Game/Screens/IHasSubScreenStack.cs
new file mode 100644
index 0000000000..c5e2015109
--- /dev/null
+++ b/osu.Game/Screens/IHasSubScreenStack.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Screens;
+
+namespace osu.Game.Screens
+{
+    /// <summary>
+    /// A screen which manages a nested stack of screens within itself.
+    /// </summary>
+    public interface IHasSubScreenStack
+    {
+        ScreenStack SubScreenStack { get; }
+    }
+}
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 71fd0d5c76..90e499c67f 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -28,7 +28,7 @@ using osuTK;
 namespace osu.Game.Screens.OnlinePlay
 {
     [Cached]
-    public abstract class OnlinePlayScreen : OsuScreen
+    public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
     {
         public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
 
@@ -355,5 +355,7 @@ namespace osu.Game.Screens.OnlinePlay
                 protected override double TransformDuration => 200;
             }
         }
+
+        ScreenStack IHasSubScreenStack.SubScreenStack => screenStack;
     }
 }

From 5eee46074cbe5821394539ed4812c3d1cc8af844 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Feb 2021 19:45:29 +0900
Subject: [PATCH 017/434] Ensure the current screen is current when a sub
 screen is found as the target

---
 osu.Game/PerformFromMenuRunner.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index 39889ea7fc..fe75a3a607 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -103,7 +103,11 @@ namespace osu.Game
                 if (current is IHasSubScreenStack currentSubScreen)
                 {
                     if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen))
+                    {
+                        // should be correct in theory, but currently untested/unused in existing implementations.
+                        current.MakeCurrent();
                         return true;
+                    }
                 }
 
                 if (validScreens.Any(t => t.IsAssignableFrom(type)))

From 32556b1898cfb65e652a9bae2dabe827d663d27b Mon Sep 17 00:00:00 2001
From: Susko3 <16479013+Susko3@users.noreply.github.com>
Date: Sat, 20 Feb 2021 02:32:44 +0100
Subject: [PATCH 018/434] add `Exported = true` to Activity manifest

---
 osu.Android/OsuGameActivity.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index ad929bbac3..d087c6218d 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -17,7 +17,7 @@ using osu.Game.Database;
 
 namespace osu.Android
 {
-    [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
+    [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
     [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
     [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
     [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })]

From 66643a97b0af5b90793435d5b6abefae582ca163 Mon Sep 17 00:00:00 2001
From: Samuel Cattini-Schultz <syriiins@gmail.com>
Date: Sat, 6 Feb 2021 15:06:16 +1100
Subject: [PATCH 019/434] Add a list of mods to Skill class

Although this isn't necessary for existing official rulesets and calculators, custom calculators can have use cases for accessing mods in difficulty calculation.
For example, accounting for the effects of visual mods.
---
 .../Difficulty/CatchDifficultyCalculator.cs         |  4 ++--
 .../Difficulty/Skills/Movement.cs                   |  4 +++-
 .../Difficulty/ManiaDifficultyCalculator.cs         |  4 ++--
 osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs |  4 +++-
 .../Difficulty/OsuDifficultyCalculator.cs           |  6 +++---
 osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs      |  6 ++++++
 osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs    |  6 ++++++
 osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs |  6 ++++++
 osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs |  6 ++++++
 .../Difficulty/Skills/Stamina.cs                    |  5 ++++-
 .../Difficulty/TaikoDifficultyCalculator.cs         | 10 +++++-----
 .../DifficultyAdjustmentModCombinationsTest.cs      |  2 +-
 .../Rulesets/Difficulty/DifficultyCalculator.cs     |  5 +++--
 osu.Game/Rulesets/Difficulty/Skills/Skill.cs        | 13 +++++++++++++
 14 files changed, 63 insertions(+), 18 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index a317ef252d..10aae70722 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
             }
         }
 
-        protected override Skill[] CreateSkills(IBeatmap beatmap)
+        protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
         {
             halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
 
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
 
             return new Skill[]
             {
-                new Movement(halfCatcherWidth),
+                new Movement(mods, halfCatcherWidth),
             };
         }
 
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index e679231638..9ad719be1a 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -5,6 +5,7 @@ using System;
 using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Catch.Difficulty.Skills
 {
@@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
         private float lastDistanceMoved;
         private double lastStrainTime;
 
-        public Movement(float halfCatcherWidth)
+        public Movement(Mod[] mods, float halfCatcherWidth)
+            : base(mods)
         {
             HalfCatcherWidth = halfCatcherWidth;
         }
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
index ade830764d..8c0b9ed8b7 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
         // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
         protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
 
-        protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+        protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
         {
-            new Strain(((ManiaBeatmap)beatmap).TotalColumns)
+            new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
         };
 
         protected override Mod[] DifficultyAdjustmentMods
diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
index 7ebc1ff752..d6ea58ee78 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
@@ -6,6 +6,7 @@ using osu.Framework.Utils;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
 using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Objects;
 
 namespace osu.Game.Rulesets.Mania.Difficulty.Skills
@@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
         private double individualStrain;
         private double overallStrain;
 
-        public Strain(int totalColumns)
+        public Strain(Mod[] mods, int totalColumns)
+            : base(mods)
         {
             holdEndTimes = new double[totalColumns];
             individualStrains = new double[totalColumns];
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 6a7d76151c..75d6786d95 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
             }
         }
 
-        protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+        protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
         {
-            new Aim(),
-            new Speed()
+            new Aim(mods),
+            new Speed(mods)
         };
 
         protected override Mod[] DifficultyAdjustmentMods => new Mod[]
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
index e74f4933b2..90cba13c7c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
@@ -4,6 +4,7 @@
 using System;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Osu.Objects;
 
@@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
         private const double angle_bonus_begin = Math.PI / 3;
         private const double timing_threshold = 107;
 
+        public Aim(Mod[] mods)
+            : base(mods)
+        {
+        }
+
         protected override double SkillMultiplier => 26.25;
         protected override double StrainDecayBase => 0.15;
 
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 01f2fb8dc8..200bc7997d 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -4,6 +4,7 @@
 using System;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Osu.Objects;
 
@@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
         private const double max_speed_bonus = 45; // ~330BPM
         private const double speed_balancing_factor = 40;
 
+        public Speed(Mod[] mods)
+            : base(mods)
+        {
+        }
+
         protected override double StrainValueOf(DifficultyHitObject current)
         {
             if (current.BaseObject is Spinner)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index 32421ee00a..cc0738e252 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -5,6 +5,7 @@ using System;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
 using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Taiko.Objects;
 
@@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
         /// </summary>
         private int currentMonoLength;
 
+        public Colour(Mod[] mods)
+            : base(mods)
+        {
+        }
+
         protected override double StrainValueOf(DifficultyHitObject current)
         {
             // changing from/to a drum roll or a swell does not constitute a colour change.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index 5569b27ad5..f2b8309ac5 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -5,6 +5,7 @@ using System;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
 using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Taiko.Objects;
 
@@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
         /// </summary>
         private int notesSinceRhythmChange;
 
+        public Rhythm(Mod[] mods)
+            : base(mods)
+        {
+        }
+
         protected override double StrainValueOf(DifficultyHitObject current)
         {
             // drum rolls and swells are exempt.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 0b61eb9930..c34cce0cd6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -5,6 +5,7 @@ using System.Linq;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Skills;
 using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Taiko.Objects;
 
@@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
         /// <summary>
         /// Creates a <see cref="Stamina"/> skill.
         /// </summary>
+        /// <param name="mods">Mods for use in skill calculations.</param>
         /// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
-        public Stamina(bool rightHand)
+        public Stamina(Mod[] mods, bool rightHand)
+            : base(mods)
         {
             hand = rightHand ? 1 : 0;
         }
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index e5485db4df..fc198d2493 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
         {
         }
 
-        protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
+        protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
         {
-            new Colour(),
-            new Rhythm(),
-            new Stamina(true),
-            new Stamina(false),
+            new Colour(mods),
+            new Rhythm(mods),
+            new Stamina(mods, true),
+            new Stamina(mods, false),
         };
 
         protected override Mod[] DifficultyAdjustmentMods => new Mod[]
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 5c7adb3f49..1c0bfd56dd 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual
                 throw new NotImplementedException();
             }
 
-            protected override Skill[] CreateSkills(IBeatmap beatmap)
+            protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
             {
                 throw new NotImplementedException();
             }
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index f15e5e1df0..a25dc3e6db 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty
 
         private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
         {
-            var skills = CreateSkills(beatmap);
+            var skills = CreateSkills(beatmap, mods);
 
             if (!beatmap.HitObjects.Any())
                 return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
@@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty
         /// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
         /// </summary>
         /// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
+        /// <param name="mods">Mods to calculate difficulty with.</param>
         /// <returns>The <see cref="Skill"/>s.</returns>
-        protected abstract Skill[] CreateSkills(IBeatmap beatmap);
+        protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
     }
 }
diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
index 1063a24b27..95117be073 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Game.Rulesets.Difficulty.Preprocessing;
 using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Difficulty.Skills
 {
@@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
         /// </summary>
         protected double CurrentStrain { get; private set; } = 1;
 
+        /// <summary>
+        /// Mods for use in skill calculations.
+        /// </summary>
+        protected IReadOnlyList<Mod> Mods => mods;
+
         private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
 
         private readonly List<double> strainPeaks = new List<double>();
 
+        private readonly Mod[] mods;
+
+        protected Skill(Mod[] mods)
+        {
+            this.mods = mods;
+        }
+
         /// <summary>
         /// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
         /// </summary>

From 8d463987dd6c7260f25c90a31804d180c147b5df Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 13:21:50 +0900
Subject: [PATCH 020/434] Fix being able to select incompatible freemods

---
 .../Screens/OnlinePlay/OnlinePlaySongSelect.cs    | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
index f0c77b79bf..3f30ef1176 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
@@ -75,9 +75,18 @@ namespace osu.Game.Screens.OnlinePlay
             Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
             FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty<Mod>();
 
+            Mods.BindValueChanged(onModsChanged);
             Ruleset.BindValueChanged(onRulesetChanged);
         }
 
+        private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
+        {
+            FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
+
+            // Reset the validity delegate to update the overlay's display.
+            freeModSelectOverlay.IsValidMod = IsValidFreeMod;
+        }
+
         private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
         {
             FreeMods.Value = Array.Empty<Mod>();
@@ -155,6 +164,10 @@ namespace osu.Game.Screens.OnlinePlay
         /// </summary>
         /// <param name="mod">The <see cref="Mod"/> to check.</param>
         /// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
-        protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod);
+        protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod);
+
+        private bool checkCompatibleFreeMod(Mod mod)
+            => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods.
+               && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods.
     }
 }

From ca92ad715a9a11bd772b2f53d04f3cb5a8e13431 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 13:32:54 +0900
Subject: [PATCH 021/434] Add test

---
 osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs |  2 +-
 .../TestSceneMultiplayerMatchSongSelect.cs    | 28 +++++++++++++++++++
 2 files changed, 29 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index df0a41455f..4b0939db16 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
 
 namespace osu.Game.Rulesets.Osu.Mods
 {
-    internal class OsuModTraceable : ModWithVisibilityAdjustment
+    public class OsuModTraceable : ModWithVisibilityAdjustment
     {
         public override string Name => "Traceable";
         public override string Acronym => "TC";
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 95c333e9f4..faa5d9e6fc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -7,17 +7,23 @@ using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
+using osu.Framework.Bindables;
 using osu.Framework.Extensions;
+using osu.Framework.Extensions.TypeExtensions;
 using osu.Framework.Platform;
 using osu.Framework.Screens;
+using osu.Framework.Testing;
 using osu.Framework.Utils;
 using osu.Game.Beatmaps;
+using osu.Game.Overlays.Mods;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Rulesets.Taiko;
 using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Screens.OnlinePlay;
 using osu.Game.Screens.OnlinePlay.Multiplayer;
 using osu.Game.Screens.Select;
 
@@ -137,8 +143,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime);
         }
 
+        [TestCase(typeof(OsuModHidden), typeof(OsuModHidden))] // Same mod.
+        [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
+        public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
+        {
+            AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
+            AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
+
+            AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0);
+            assertHasFreeModButton(allowedMod, false);
+            assertHasFreeModButton(requiredMod, false);
+        }
+
+        private void assertHasFreeModButton(Type type, bool hasButton = true)
+        {
+            AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay",
+                () => songSelect.ChildrenOfType<FreeModSelectOverlay>().Single().ChildrenOfType<ModButton>().All(b => b.Mod.GetType() != type));
+        }
+
         private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect
         {
+            public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods;
+
+            public new Bindable<IReadOnlyList<Mod>> FreeMods => base.FreeMods;
+
             public new BeatmapCarousel Carousel => base.Carousel;
         }
     }

From e2c5dded7f4e5b4ac1a5123e70e54728f251bb2a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 14:14:36 +0900
Subject: [PATCH 022/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 1513f6444d..183ac61c90 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.220.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.222.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9c3d0c2020..37d730bf42 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.220.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.1" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 99ab88a064..ca11952cc8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.220.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.222.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -91,7 +91,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.220.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 63dd55c92c9f725926f89dfa14b4ba84d65760a3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 14:18:52 +0900
Subject: [PATCH 023/434] Add missing methods from updated audio component
 interface implementation

---
 .../TestSceneDrawableRulesetDependencies.cs    |  4 ++++
 .../Rulesets/UI/DrawableRulesetDependencies.cs |  4 ++++
 osu.Game/Skinning/PoolableSkinnableSample.cs   |  4 ++++
 osu.Game/Skinning/SkinnableSound.cs            | 18 ++++++++++++++++++
 4 files changed, 30 insertions(+)

diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index 4aebed0d31..f421a30283 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -118,6 +118,10 @@ namespace osu.Game.Tests.Rulesets
             public BindableNumber<double> Frequency => throw new NotImplementedException();
             public BindableNumber<double> Tempo => throw new NotImplementedException();
 
+            public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
+            public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
             public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
 
             public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => throw new NotImplementedException();
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index bbaca7c80f..b31884d246 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -134,6 +134,10 @@ namespace osu.Game.Rulesets.UI
 
             public IBindable<double> AggregateTempo => throw new NotSupportedException();
 
+            public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
+            public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
             public int PlaybackConcurrency
             {
                 get => throw new NotSupportedException();
diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index abff57091b..5a0cf94d6a 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -165,6 +165,10 @@ namespace osu.Game.Skinning
 
         public BindableNumber<double> Tempo => sampleContainer.Tempo;
 
+        public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component);
+
+        public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component);
+
         public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable);
 
         public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable);
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index d3dfcb1dc0..57e20a8d31 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -176,6 +176,16 @@ namespace osu.Game.Skinning
 
         public BindableNumber<double> Tempo => samplesContainer.Tempo;
 
+        public void BindAdjustments(IAggregateAudioAdjustment component)
+        {
+            samplesContainer.BindAdjustments(component);
+        }
+
+        public void UnbindAdjustments(IAggregateAudioAdjustment component)
+        {
+            samplesContainer.UnbindAdjustments(component);
+        }
+
         public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
             => samplesContainer.AddAdjustment(type, adjustBindable);
 
@@ -192,6 +202,14 @@ namespace osu.Game.Skinning
 
         public bool IsPlayed => samplesContainer.Any(s => s.Played);
 
+        public IBindable<double> AggregateVolume => samplesContainer.AggregateVolume;
+
+        public IBindable<double> AggregateBalance => samplesContainer.AggregateBalance;
+
+        public IBindable<double> AggregateFrequency => samplesContainer.AggregateFrequency;
+
+        public IBindable<double> AggregateTempo => samplesContainer.AggregateTempo;
+
         #endregion
     }
 }

From 541237ef16c62d259e4e84fa13943c32a7a54be0 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 14:48:04 +0900
Subject: [PATCH 024/434] Use a shorter test beatmap for tests which need to
 run to completion

---
 .../Beatmaps/IO/ImportBeatmapTest.cs          |  15 +++++++++++
 ...241526 Soleily - Renatus_virtual_quick.osz | Bin 0 -> 89215 bytes
 osu.Game.Tests/Resources/TestResources.cs     |  25 +++++++++++++++++-
 .../Navigation/TestSceneScreenNavigation.cs   |   2 +-
 4 files changed, 40 insertions(+), 2 deletions(-)
 create mode 100644 osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz

diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index c32e359de6..0c35e9471d 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -852,6 +852,21 @@ namespace osu.Game.Tests.Beatmaps.IO
             }
         }
 
+        public static async Task<BeatmapSetInfo> LoadQuickOszIntoOsu(OsuGameBase osu)
+        {
+            var temp = TestResources.GetQuickTestBeatmapForImport();
+
+            var manager = osu.Dependencies.Get<BeatmapManager>();
+
+            var importedSet = await manager.Import(new ImportTask(temp));
+
+            ensureLoaded(osu);
+
+            waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
+
+            return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
+        }
+
         public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
         {
             var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
diff --git a/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz
new file mode 100644
index 0000000000000000000000000000000000000000..e9f5fb03282e189ffc23d77a0db40b3fa2488850
GIT binary patch
literal 89215
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
Ni{DEH3<&Xe{{o+^{JH=D

literal 0
HcmV?d00001

diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index e882229570..14bc2c8733 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -13,8 +13,31 @@ namespace osu.Game.Tests.Resources
 
         public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}");
 
-        public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz");
+        public static Stream GetTestBeatmapStream(bool virtualTrack = false, bool quick = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}{(quick ? "_quick" : "")}.osz");
 
+        /// <summary>
+        /// Retrieve a path to a copy of a shortened (~10 second) beatmap archive with a virtual track.
+        /// </summary>
+        /// <remarks>
+        /// This is intended for use in tests which need to run to completion as soon as possible and don't need to test a full length beatmap.</remarks>
+        /// <returns>A path to a copy of a beatmap archive (osz). Should be deleted after use.</returns>
+        public static string GetQuickTestBeatmapForImport()
+        {
+            var tempPath = Path.GetTempFileName() + ".osz";
+
+            using (var stream = GetTestBeatmapStream(true, true))
+            using (var newFile = File.Create(tempPath))
+                stream.CopyTo(newFile);
+
+            Assert.IsTrue(File.Exists(tempPath));
+            return tempPath;
+        }
+
+        /// <summary>
+        /// Retrieve a path to a copy of a full-fledged beatmap archive.
+        /// </summary>
+        /// <param name="virtualTrack">Whether the audio track should be virtual.</param>
+        /// <returns>A path to a copy of a beatmap archive (osz). Should be deleted after use.</returns>
         public static string GetTestBeatmapForImport(bool virtualTrack = false)
         {
             var tempPath = Path.GetTempFileName() + ".osz";
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index d8380b2dd3..e5959a3edf 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Navigation
 
             PushAndConfirm(() => new TestSongSelect());
 
-            AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait());
+            AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
 
             AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
 

From cdbf8de29db80994e903c1e92837d6208e78312f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 14:53:32 +0900
Subject: [PATCH 025/434] Update other tests which can benefit from using a
 shorter beatmap

---
 .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs     | 2 +-
 osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +-
 .../Visual/Collections/TestSceneManageCollectionsDialog.cs      | 2 +-
 .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs       | 2 +-
 osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs   | 2 +-
 osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs      | 2 +-
 .../Visual/UserInterface/TestSceneDeleteLocalScore.cs           | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 3ffb512b7f..8c30802ce3 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Online
         {
             beatmaps.AllowImport = new TaskCompletionSource<bool>();
 
-            testBeatmapFile = TestResources.GetTestBeatmapForImport();
+            testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
 
             testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
             testBeatmapSet = testBeatmapInfo.BeatmapSet;
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 7ade7725d9..ba4d12b19f 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Background
             Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
             Dependencies.Cache(new OsuConfigManager(LocalStorage));
 
-            manager.Import(TestResources.GetTestBeatmapForImport()).Wait();
+            manager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
 
             Beatmap.SetDefault();
         }
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index fef1605f0c..1655adf811 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Collections
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
 
-            beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
+            beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
 
             base.Content.AddRange(new Drawable[]
             {
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
index 3b3b1bee86..b44e5b1e5b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
-            beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait();
+            beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
 
             Add(beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker
             {
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
index 63bda08c88..0c199bfb62 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
             ensureSoleilyRemoved();
             createButtonWithBeatmap(createSoleily());
             AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
-            AddStep("import soleily", () => beatmaps.Import(TestResources.GetTestBeatmapForImport()));
+            AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
             AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526));
             createButtonWithBeatmap(createSoleily());
             AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index 5d0fb248df..c13bdf0955 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
             Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
 
-            beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
+            beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
 
             base.Content.AddRange(new Drawable[]
             {
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 81862448a8..d615f1f440 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface
             dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), dependencies.Get<GameHost>(), Beatmap.Default));
             dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
 
-            beatmap = beatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result.Beatmaps[0];
+            beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
 
             for (int i = 0; i < 50; i++)
             {

From fde026d44342534f7d06ddc0873e18f3f24e7070 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 14:54:48 +0900
Subject: [PATCH 026/434] Remove redundant interface specification

---
 osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 5a0cf94d6a..9103a6a960 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Skinning
     /// <summary>
     /// A sample corresponding to an <see cref="ISampleInfo"/> that supports being pooled and responding to skin changes.
     /// </summary>
-    public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent
+    public class PoolableSkinnableSample : SkinReloadableDrawable, IAdjustableAudioComponent
     {
         /// <summary>
         /// The currently-loaded <see cref="DrawableSample"/>.

From adf2dc36c9112200699ac8680b81a32bda9b937f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 15:43:58 +0900
Subject: [PATCH 027/434] Fix PlaylistResults tests performing delays in
 real-time when headless

---
 .../TestScenePlaylistsResultsScreen.cs        | 87 +++++++------------
 1 file changed, 32 insertions(+), 55 deletions(-)

diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index cdcded8f61..e34da1ef0c 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists
             AddStep("bind user score info handler", () =>
             {
                 userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
-                bindHandler(3000, userScore);
+                bindHandler(true, userScore);
             });
 
             createResults(() => userScore);
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists
         [Test]
         public void TestShowNullUserScoreWithDelay()
         {
-            AddStep("bind delayed handler", () => bindHandler(3000));
+            AddStep("bind delayed handler", () => bindHandler(true));
 
             createResults();
             waitForDisplay();
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Playlists
             createResults();
             waitForDisplay();
 
-            AddStep("bind delayed handler", () => bindHandler(3000));
+            AddStep("bind delayed handler", () => bindHandler(true));
 
             for (int i = 0; i < 2; i++)
             {
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Playlists
             createResults(() => userScore);
             waitForDisplay();
 
-            AddStep("bind delayed handler", () => bindHandler(3000));
+            AddStep("bind delayed handler", () => bindHandler(true));
 
             for (int i = 0; i < 2; i++)
             {
@@ -169,70 +169,47 @@ namespace osu.Game.Tests.Visual.Playlists
             AddWaitStep("wait for display", 5);
         }
 
-        private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
+        private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
         {
             requestComplete = false;
 
-            if (failRequests)
-            {
-                triggerFail(request, delay);
-                return;
-            }
+            double delay = delayed ? 3000 : 0;
 
-            switch (request)
+            Scheduler.AddDelayed(() =>
             {
-                case ShowPlaylistUserScoreRequest s:
-                    if (userScore == null)
-                        triggerFail(s, delay);
-                    else
-                        triggerSuccess(s, createUserResponse(userScore), delay);
-                    break;
+                if (failRequests)
+                {
+                    triggerFail(request);
+                    return;
+                }
 
-                case IndexPlaylistScoresRequest i:
-                    triggerSuccess(i, createIndexResponse(i), delay);
-                    break;
-            }
+                switch (request)
+                {
+                    case ShowPlaylistUserScoreRequest s:
+                        if (userScore == null)
+                            triggerFail(s);
+                        else
+                            triggerSuccess(s, createUserResponse(userScore));
+                        break;
+
+                    case IndexPlaylistScoresRequest i:
+                        triggerSuccess(i, createIndexResponse(i));
+                        break;
+                }
+            }, delay);
         };
 
-        private void triggerSuccess<T>(APIRequest<T> req, T result, double delay)
+        private void triggerSuccess<T>(APIRequest<T> req, T result)
             where T : class
         {
-            if (delay == 0)
-                success();
-            else
-            {
-                Task.Run(async () =>
-                {
-                    await Task.Delay(TimeSpan.FromMilliseconds(delay));
-                    Schedule(success);
-                });
-            }
-
-            void success()
-            {
-                requestComplete = true;
-                req.TriggerSuccess(result);
-            }
+            requestComplete = true;
+            req.TriggerSuccess(result);
         }
 
-        private void triggerFail(APIRequest req, double delay)
+        private void triggerFail(APIRequest req)
         {
-            if (delay == 0)
-                fail();
-            else
-            {
-                Task.Run(async () =>
-                {
-                    await Task.Delay(TimeSpan.FromMilliseconds(delay));
-                    Schedule(fail);
-                });
-            }
-
-            void fail()
-            {
-                requestComplete = true;
-                req.TriggerFailure(new WebException("Failed."));
-            }
+            requestComplete = true;
+            req.TriggerFailure(new WebException("Failed."));
         }
 
         private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)

From ccb83ef3a374f173b18473665c06c5002d68aecb Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 15:47:47 +0900
Subject: [PATCH 028/434] Fix checkbox not being updated

---
 osu.Game/Overlays/Mods/ModSection.cs          | 20 +++++++++----------
 osu.Game/Overlays/Mods/ModSelectOverlay.cs    |  8 ++++++++
 .../OnlinePlay/FreeModSelectOverlay.cs        | 14 ++++++++++++-
 3 files changed, 31 insertions(+), 11 deletions(-)

diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index c3e56abd05..aa8a5efd39 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -23,13 +23,15 @@ namespace osu.Game.Overlays.Mods
 
         public FillFlowContainer<ModButtonEmpty> ButtonsContainer { get; }
 
+        protected IReadOnlyList<ModButton> Buttons { get; private set; } = Array.Empty<ModButton>();
+
         public Action<Mod> Action;
 
         public Key[] ToggleKeys;
 
         public readonly ModType ModType;
 
-        public IEnumerable<Mod> SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null);
+        public IEnumerable<Mod> SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null);
 
         private CancellationTokenSource modsLoadCts;
 
@@ -77,7 +79,7 @@ namespace osu.Game.Overlays.Mods
                     ButtonsContainer.ChildrenEnumerable = c;
                 }, (modsLoadCts = new CancellationTokenSource()).Token);
 
-                buttons = modContainers.OfType<ModButton>().ToArray();
+                Buttons = modContainers.OfType<ModButton>().ToArray();
 
                 header.FadeIn(200);
                 this.FadeIn(200);
@@ -88,8 +90,6 @@ namespace osu.Game.Overlays.Mods
         {
         }
 
-        private ModButton[] buttons = Array.Empty<ModButton>();
-
         protected override bool OnKeyDown(KeyDownEvent e)
         {
             if (e.ControlPressed) return false;
@@ -97,8 +97,8 @@ namespace osu.Game.Overlays.Mods
             if (ToggleKeys != null)
             {
                 var index = Array.IndexOf(ToggleKeys, e.Key);
-                if (index > -1 && index < buttons.Length)
-                    buttons[index].SelectNext(e.ShiftPressed ? -1 : 1);
+                if (index > -1 && index < Buttons.Count)
+                    Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1);
             }
 
             return base.OnKeyDown(e);
@@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods
         {
             pendingSelectionOperations.Clear();
 
-            foreach (var button in buttons.Where(b => !b.Selected))
+            foreach (var button in Buttons.Where(b => !b.Selected))
                 pendingSelectionOperations.Enqueue(() => button.SelectAt(0));
         }
 
@@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Mods
         public void DeselectAll()
         {
             pendingSelectionOperations.Clear();
-            DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null));
+            DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null));
         }
 
         /// <summary>
@@ -161,7 +161,7 @@ namespace osu.Game.Overlays.Mods
         /// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
         public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false)
         {
-            foreach (var button in buttons)
+            foreach (var button in Buttons)
             {
                 if (button.SelectedMod == null) continue;
 
@@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Mods
         /// <param name="newSelectedMods">The new list of selected mods to select.</param>
         public void UpdateSelectedButtons(IReadOnlyList<Mod> newSelectedMods)
         {
-            foreach (var button in buttons)
+            foreach (var button in Buttons)
                 updateButtonSelection(button, newSelectedMods);
         }
 
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index eef91deb4c..26b8632d7f 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -456,6 +456,7 @@ namespace osu.Game.Overlays.Mods
             }
 
             updateSelectedButtons();
+            OnAvailableModsChanged();
         }
 
         /// <summary>
@@ -533,6 +534,13 @@ namespace osu.Game.Overlays.Mods
         private void playSelectedSound() => sampleOn?.Play();
         private void playDeselectedSound() => sampleOff?.Play();
 
+        /// <summary>
+        /// Invoked after <see cref="availableMods"/> has changed.
+        /// </summary>
+        protected virtual void OnAvailableModsChanged()
+        {
+        }
+
         /// <summary>
         /// Invoked when a new <see cref="Mod"/> has been selected.
         /// </summary>
diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
index ab7be13479..66262e7dc4 100644
--- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs
@@ -75,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay
                 section.DeselectAll();
         }
 
+        protected override void OnAvailableModsChanged()
+        {
+            base.OnAvailableModsChanged();
+
+            foreach (var section in ModSectionsContainer.Children)
+                ((FreeModSection)section).UpdateCheckboxState();
+        }
+
         protected override ModSection CreateModSection(ModType type) => new FreeModSection(type);
 
         private class FreeModSection : ModSection
@@ -108,10 +116,14 @@ namespace osu.Game.Screens.OnlinePlay
             protected override void ModButtonStateChanged(Mod mod)
             {
                 base.ModButtonStateChanged(mod);
+                UpdateCheckboxState();
+            }
 
+            public void UpdateCheckboxState()
+            {
                 if (!SelectionAnimationRunning)
                 {
-                    var validButtons = ButtonsContainer.OfType<ModButton>().Where(b => b.Mod.HasImplementation);
+                    var validButtons = Buttons.Where(b => b.Mod.HasImplementation);
                     checkbox.Current.Value = validButtons.All(b => b.Selected);
                 }
             }

From d985b8ab2aafd86c9f4d24fdcd39634de8a0b10c Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 17:14:39 +0900
Subject: [PATCH 029/434] Increase beatmapset download timeout

---
 osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs
index 707c59436d..e8871bef05 100644
--- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs
+++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using osu.Framework.IO.Network;
 using osu.Game.Beatmaps;
 
 namespace osu.Game.Online.API.Requests
@@ -15,6 +16,13 @@ namespace osu.Game.Online.API.Requests
             this.noVideo = noVideo;
         }
 
+        protected override WebRequest CreateWebRequest()
+        {
+            var req = base.CreateWebRequest();
+            req.Timeout = 60000;
+            return req;
+        }
+
         protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
     }
 }

From 0bda9e4b794f16d108f234563746202bf3b8a160 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 18:31:33 +0900
Subject: [PATCH 030/434] Implement some new methods

---
 osu.Game/Skinning/LegacySkin.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 2edc36a770..6bdc4575c9 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -529,6 +529,14 @@ namespace osu.Game.Skinning
             {
                 sample.RemoveAllAdjustments(type);
             }
+
+            public IBindable<double> AggregateVolume => sample.AggregateVolume;
+
+            public IBindable<double> AggregateBalance => sample.AggregateBalance;
+
+            public IBindable<double> AggregateFrequency => sample.AggregateFrequency;
+
+            public IBindable<double> AggregateTempo => sample.AggregateTempo;
         }
 
         private IEnumerable<string> getLegacyLookupNames(HitSampleInfo hitSample)

From f48e017ac901bcd5387a87d53b2a5ab5dc771d06 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Feb 2021 18:34:05 +0900
Subject: [PATCH 031/434] Move nested class to bottom of file

---
 osu.Game/Skinning/LegacySkin.cs | 78 ++++++++++++++++-----------------
 1 file changed, 39 insertions(+), 39 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 6bdc4575c9..571d65e28b 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -468,7 +468,45 @@ namespace osu.Game.Skinning
             return null;
         }
 
-        /// <summary>
+        private IEnumerable<string> getLegacyLookupNames(HitSampleInfo hitSample)
+        {
+            var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames);
+
+            if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix))
+            {
+                // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin.
+                // using .EndsWith() is intentional as it ensures parity in all edge cases
+                // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not).
+                lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal));
+            }
+
+            foreach (var l in lookupNames)
+                yield return l;
+
+            // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort.
+            // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable,
+            // which is why this is done locally here.
+            yield return hitSample.Name;
+        }
+
+        private IEnumerable<string> getFallbackNames(string componentName)
+        {
+            // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin.
+            yield return componentName;
+
+            // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
+            string lastPiece = componentName.Split('/').Last();
+            yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
+        }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            Textures?.Dispose();
+            Samples?.Dispose();
+        }
+
+          /// <summary>
         /// A sample wrapper which keeps a reference to the contained skin to avoid finalizer garbage collection of the managing SampleStore.
         /// </summary>
         private class LegacySkinSample : ISample
@@ -538,43 +576,5 @@ namespace osu.Game.Skinning
 
             public IBindable<double> AggregateTempo => sample.AggregateTempo;
         }
-
-        private IEnumerable<string> getLegacyLookupNames(HitSampleInfo hitSample)
-        {
-            var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames);
-
-            if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix))
-            {
-                // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin.
-                // using .EndsWith() is intentional as it ensures parity in all edge cases
-                // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not).
-                lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal));
-            }
-
-            foreach (var l in lookupNames)
-                yield return l;
-
-            // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort.
-            // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable,
-            // which is why this is done locally here.
-            yield return hitSample.Name;
-        }
-
-        private IEnumerable<string> getFallbackNames(string componentName)
-        {
-            // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin.
-            yield return componentName;
-
-            // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle").
-            string lastPiece = componentName.Split('/').Last();
-            yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-            Textures?.Dispose();
-            Samples?.Dispose();
-        }
     }
 }

From 1fd76ea3fb9db1d7e80e92fbd9e9cdb40353683c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 17:14:00 +0900
Subject: [PATCH 032/434] Apply changes to UI components overriding functions
 with changing signatures

---
 osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs    | 2 +-
 .../Ranking/TestSceneExpandedPanelMiddleContent.cs       | 2 +-
 .../Visual/Settings/TestSceneKeyBindingPanel.cs          | 4 ++--
 .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs       | 2 +-
 .../Visual/UserInterface/TestSceneDeleteLocalScore.cs    | 2 +-
 osu.Game/Collections/CollectionFilterDropdown.cs         | 5 +++--
 osu.Game/Graphics/Sprites/GlowingSpriteText.cs           | 3 ++-
 osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs   | 3 ++-
 osu.Game/Graphics/UserInterface/OsuButton.cs             | 5 +++--
 osu.Game/Graphics/UserInterface/OsuDropdown.cs           | 5 +++--
 osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs | 3 ++-
 osu.Game/Graphics/UserInterface/ShowMoreButton.cs        | 3 ++-
 osu.Game/Graphics/UserInterface/TriangleButton.cs        | 2 +-
 .../Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs   | 1 -
 .../Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs   | 1 -
 osu.Game/Overlays/BeatmapSet/BasicStats.cs               | 3 ++-
 .../BeatmapSet/Scores/TopScoreStatisticsSection.cs       | 1 -
 osu.Game/Overlays/Chat/Selection/ChannelSection.cs       | 9 ++-------
 .../Overlays/Chat/Selection/ChannelSelectionOverlay.cs   | 6 +-----
 .../Overlays/Comments/Buttons/CommentRepliesButton.cs    | 3 ++-
 osu.Game/Overlays/KeyBinding/KeyBindingRow.cs            | 2 +-
 osu.Game/Overlays/Notifications/NotificationSection.cs   | 7 ++++---
 osu.Game/Overlays/NowPlayingOverlay.cs                   | 1 -
 osu.Game/Overlays/OverlaySortTabControl.cs               | 3 ++-
 .../Sections/Historical/DrawableMostPlayedBeatmap.cs     | 1 -
 .../Profile/Sections/Ranks/DrawableProfileScore.cs       | 1 -
 .../Settings/Sections/Audio/AudioDevicesSettings.cs      | 3 ++-
 .../Settings/Sections/Graphics/LayoutSettings.cs         | 3 ++-
 osu.Game/Overlays/Settings/Sections/SkinSection.cs       | 2 +-
 osu.Game/Overlays/Settings/SettingsCheckbox.cs           | 8 +++++---
 osu.Game/Overlays/Settings/SettingsItem.cs               | 5 +++--
 osu.Game/Overlays/Toolbar/ToolbarButton.cs               | 7 ++++---
 osu.Game/Screens/Menu/SongTicker.cs                      | 1 -
 osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs   | 1 -
 .../Playlists/PlaylistsMatchSettingsOverlay.cs           | 3 ++-
 osu.Game/Screens/Play/BeatmapMetadataDisplay.cs          | 1 -
 .../Ranking/Expanded/ExpandedPanelMiddleContent.cs       | 1 -
 osu.Game/Screens/Select/Carousel/SetPanelContent.cs      | 1 -
 osu.Game/Screens/Select/Details/AdvancedStats.cs         | 3 ++-
 osu.Game/Screens/Select/FooterButton.cs                  | 5 +++--
 osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs  | 5 +++--
 osu.Game/Skinning/SkinnableSpriteText.cs                 | 5 +++--
 42 files changed, 68 insertions(+), 66 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
index bea6186501..43d8d1e27f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
 
         public string Text
         {
-            get => number.Text;
+            get => number.Text.ToString();
             set => number.Text = value;
         }
 
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 7be44a62de..f9fe42131f 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
             }));
 
             AddAssert("mapped by text not present", () =>
-                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text, "mapped", "by")));
+                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
         }
 
         private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index 8330b9b360..f495e0fb23 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Settings
 
             clickClearButton();
 
-            AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().Text.Text));
+            AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().Text.Text.ToString()));
 
             AddStep("click second binding", () =>
             {
@@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Settings
 
             clickClearButton();
 
-            AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1).Text.Text));
+            AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(1).Text.Text.ToString()));
 
             void clickClearButton()
             {
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index 0b2c0ce63b..fff4a9ba61 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         public void TestNullBeatmap()
         {
             selectBeatmap(null);
-            AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text));
+            AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text.ToString()));
             AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title);
             AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist);
             AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 81862448a8..1516a7d621 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
 
             AddStep("click delete option", () =>
             {
-                InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.Value.ToLowerInvariant() == "delete"));
+                InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.Value.ToString().ToLowerInvariant() == "delete"));
                 InputManager.Click(MouseButton.Left);
             });
 
diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs
index bb743d4ccc..1eceb56e33 100644
--- a/osu.Game/Collections/CollectionFilterDropdown.cs
+++ b/osu.Game/Collections/CollectionFilterDropdown.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -121,7 +122,7 @@ namespace osu.Game.Collections
             Current.TriggerChange();
         }
 
-        protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value;
+        protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value;
 
         protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d =>
         {
@@ -139,7 +140,7 @@ namespace osu.Game.Collections
             public readonly Bindable<CollectionFilterMenuItem> SelectedItem = new Bindable<CollectionFilterMenuItem>();
             private readonly Bindable<string> collectionName = new Bindable<string>();
 
-            protected override string Label
+            protected override LocalisableString Label
             {
                 get => base.Label;
                 set { } // See updateText().
diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs
index 85df2d167f..fb273d7293 100644
--- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs
+++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 using osuTK;
 
 namespace osu.Game.Graphics.Sprites
@@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Sprites
     {
         private readonly OsuSpriteText spriteText, blurredText;
 
-        public string Text
+        public LocalisableString Text
         {
             get => spriteText.Text;
             set => blurredText.Text = spriteText.Text = value;
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index b499b26f38..8df2c1c2fd 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.Sprites;
 using osuTK.Graphics;
 
@@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface
 
         protected class TextContainer : Container, IHasText
         {
-            public string Text
+            public LocalisableString Text
             {
                 get => NormalText.Text;
                 set
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 9cf8f02024..d2114134cf 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.Sprites;
 using osuTK.Graphics;
 
@@ -21,9 +22,9 @@ namespace osu.Game.Graphics.UserInterface
     /// </summary>
     public class OsuButton : Button
     {
-        public string Text
+        public LocalisableString Text
         {
-            get => SpriteText?.Text;
+            get => SpriteText.Text;
             set
             {
                 if (SpriteText != null)
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index cc76c12975..15fb00ccb0 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osuTK;
@@ -168,7 +169,7 @@ namespace osu.Game.Graphics.UserInterface
 
                 protected new class Content : FillFlowContainer, IHasText
                 {
-                    public string Text
+                    public LocalisableString Text
                     {
                         get => Label.Text;
                         set => Label.Text = value;
@@ -215,7 +216,7 @@ namespace osu.Game.Graphics.UserInterface
         {
             protected readonly SpriteText Text;
 
-            protected override string Label
+            protected override LocalisableString Label
             {
                 get => Text.Text;
                 set => Text.Text = value;
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
index bdc95ee048..b66a4a58ce 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 
 namespace osu.Game.Graphics.UserInterface
 {
@@ -35,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
             }
         }
 
-        public string Text
+        public LocalisableString Text
         {
             get => text.Text;
             set => text.Text = value;
diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
index 924c7913f3..615895074c 100644
--- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites;
 using osu.Game.Overlays;
 using osuTK;
 using System.Collections.Generic;
+using osu.Framework.Localisation;
 
 namespace osu.Game.Graphics.UserInterface
 {
@@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface
     {
         private const int duration = 200;
 
-        public string Text
+        public LocalisableString Text
         {
             get => text.Text;
             set => text.Text = value;
diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs
index 5baf794227..003a81f562 100644
--- a/osu.Game/Graphics/UserInterface/TriangleButton.cs
+++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface
             });
         }
 
-        public virtual IEnumerable<string> FilterTerms => new[] { Text };
+        public virtual IEnumerable<string> FilterTerms => new[] { Text.ToString() };
 
         public bool MatchingFilter
         {
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
index c1d366bb82..97e7ce83a5 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
index 76a30d1c11..4a887ed571 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
index a2464bef09..cf74c0d4d3 100644
--- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs
+++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Cursor;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
@@ -96,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet
 
             public string TooltipText { get; }
 
-            public string Value
+            public LocalisableString Value
             {
                 get => value.Text;
                 set => this.value.Text = value;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 93744dd6a3..c281d7b432 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
index eac48ca5cb..e18302770c 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
@@ -4,12 +4,12 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using osuTK;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Online.Chat;
+using osuTK;
 
 namespace osu.Game.Overlays.Chat.Selection
 {
@@ -29,12 +29,6 @@ namespace osu.Game.Overlays.Chat.Selection
 
         public bool FilteringActive { get; set; }
 
-        public string Header
-        {
-            get => header.Text;
-            set => header.Text = value.ToUpperInvariant();
-        }
-
         public IEnumerable<Channel> Channels
         {
             set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c));
@@ -50,6 +44,7 @@ namespace osu.Game.Overlays.Chat.Selection
                 header = new OsuSpriteText
                 {
                     Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
+                    Text = "All Channels".ToUpperInvariant()
                 },
                 ChannelFlow = new FillFlowContainer<ChannelListItem>
                 {
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
index be9ecc6746..231d7ca63c 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
@@ -131,11 +131,7 @@ namespace osu.Game.Overlays.Chat.Selection
             {
                 sectionsFlow.ChildrenEnumerable = new[]
                 {
-                    new ChannelSection
-                    {
-                        Header = "All Channels",
-                        Channels = channels,
-                    },
+                    new ChannelSection { Channels = channels, },
                 };
 
                 foreach (ChannelSection s in sectionsFlow.Children)
diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
index 57bf2af4d2..2f7f16dd6f 100644
--- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
@@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Comments.Buttons
 {
     public abstract class CommentRepliesButton : CompositeDrawable
     {
-        protected string Text
+        protected LocalisableString Text
         {
             get => text.Text;
             set => text.Text = value;
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index b808d49fa2..300fce962a 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Overlays.KeyBinding
         private FillFlowContainer cancelAndClearButtons;
         private FillFlowContainer<KeyButton> buttons;
 
-        public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text);
+        public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
 
         public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
         {
diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs
index 38ba712254..bc41311a6d 100644
--- a/osu.Game/Overlays/Notifications/NotificationSection.cs
+++ b/osu.Game/Overlays/Notifications/NotificationSection.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osuTK;
@@ -37,7 +38,7 @@ namespace osu.Game.Overlays.Notifications
 
         public NotificationSection(string title, string clearButtonText)
         {
-            this.clearButtonText = clearButtonText;
+            this.clearButtonText = clearButtonText.ToUpperInvariant();
             titleText = title;
         }
 
@@ -138,10 +139,10 @@ namespace osu.Game.Overlays.Notifications
                 };
             }
 
-            public string Text
+            public LocalisableString Text
             {
                 get => text.Text;
-                set => text.Text = value.ToUpperInvariant();
+                set => text.Text = value;
             }
         }
 
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 2866d2ad6d..74317a143c 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs
index b2212336ef..0ebabd424f 100644
--- a/osu.Game/Overlays/OverlaySortTabControl.cs
+++ b/osu.Game/Overlays/OverlaySortTabControl.cs
@@ -17,6 +17,7 @@ using osu.Game.Overlays.Comments;
 using JetBrains.Annotations;
 using System;
 using osu.Framework.Extensions;
+using osu.Framework.Localisation;
 
 namespace osu.Game.Overlays
 {
@@ -30,7 +31,7 @@ namespace osu.Game.Overlays
             set => current.Current = value;
         }
 
-        public string Title
+        public LocalisableString Title
         {
             get => text.Text;
             set => text.Text = value;
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
index 5b7c5efbe2..e485802095 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index 2c20dcc0ef..859637485f 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -7,7 +7,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
index bed74542c9..b31e7dc45b 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -6,6 +6,7 @@ using osu.Framework.Audio;
 using osu.Framework.Graphics;
 using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.UserInterface;
 
 namespace osu.Game.Overlays.Settings.Sections.Audio
@@ -76,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
 
             private class AudioDeviceDropdownControl : DropdownControl
             {
-                protected override string GenerateItemText(string item)
+                protected override LocalisableString GenerateItemText(string item)
                     => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item);
             }
         }
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 7acbf038d8..4d5c2e06eb 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -11,6 +11,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
 using osu.Framework.Platform;
 using osu.Game.Configuration;
 using osu.Game.Graphics.Containers;
@@ -234,7 +235,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
             private class ResolutionDropdownControl : DropdownControl
             {
-                protected override string GenerateItemText(Size item)
+                protected override LocalisableString GenerateItemText(Size item)
                 {
                     if (item == new Size(9999, 9999))
                         return "Default";
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 7c8309fd56..75068bd611 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Overlays.Settings.Sections
 
             private class SkinDropdownControl : DropdownControl
             {
-                protected override string GenerateItemText(SkinInfo item) => item.ToString();
+                protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString();
             }
         }
 
diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs
index 437b2e45b3..8b7ac80a5b 100644
--- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs
+++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs
@@ -2,20 +2,22 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Graphics;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.UserInterface;
 
 namespace osu.Game.Overlays.Settings
 {
     public class SettingsCheckbox : SettingsItem<bool>
     {
-        private string labelText;
+        private LocalisableString labelText;
 
         protected override Drawable CreateControl() => new OsuCheckbox();
 
-        public override string LabelText
+        public override LocalisableString LabelText
         {
             get => labelText;
-            set => ((OsuCheckbox)Control).LabelText = labelText = value;
+            // checkbox doesn't properly support localisation yet.
+            set => ((OsuCheckbox)Control).LabelText = (labelText = value).ToString();
         }
     }
 }
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index af225889da..aafd7463a6 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osuTK;
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings
 
         public string TooltipText { get; set; }
 
-        public virtual string LabelText
+        public virtual LocalisableString LabelText
         {
             get => labelText?.Text ?? string.Empty;
             set
@@ -69,7 +70,7 @@ namespace osu.Game.Overlays.Settings
             set => controlWithCurrent.Current = value;
         }
 
-        public virtual IEnumerable<string> FilterTerms => Keywords == null ? new[] { LabelText } : new List<string>(Keywords) { LabelText }.ToArray();
+        public virtual IEnumerable<string> FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List<string>(Keywords) { LabelText.ToString() }.ToArray();
 
         public IEnumerable<string> Keywords { get; set; }
 
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 83f2bdf6cb..7790a21e0a 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Backgrounds;
 using osu.Game.Graphics.Containers;
@@ -43,19 +44,19 @@ namespace osu.Game.Overlays.Toolbar
                 Texture = textures.Get(texture),
             });
 
-        public string Text
+        public LocalisableString Text
         {
             get => DrawableText.Text;
             set => DrawableText.Text = value;
         }
 
-        public string TooltipMain
+        public LocalisableString TooltipMain
         {
             get => tooltip1.Text;
             set => tooltip1.Text = value;
         }
 
-        public string TooltipSub
+        public LocalisableString TooltipSub
         {
             get => tooltip2.Text;
             set => tooltip2.Text = value;
diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs
index c4943e77d5..fd9d9a3fac 100644
--- a/osu.Game/Screens/Menu/SongTicker.cs
+++ b/osu.Game/Screens/Menu/SongTicker.cs
@@ -9,7 +9,6 @@ using osuTK;
 using osu.Game.Graphics;
 using osu.Framework.Bindables;
 using osu.Game.Beatmaps;
-using osu.Framework.Localisation;
 
 namespace osu.Game.Screens.Menu
 {
diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
index acb82360b3..b64ea37a59 100644
--- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
@@ -4,7 +4,6 @@
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
-using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs
index 2a1efbc040..5062a296a8 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs
@@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
@@ -362,7 +363,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
                 Menu.MaxHeight = 100;
             }
 
-            protected override string GenerateItemText(TimeSpan item) => item.Humanize();
+            protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize();
         }
     }
 }
diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
index eff06e26ee..bb82b00100 100644
--- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
+++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index ff6203bc25..85a9b06a70 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -6,7 +6,6 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index 4e8d27f14d..82704c24fb 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -6,7 +6,6 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs
index ab4f3f4796..1627d3ddfc 100644
--- a/osu.Game/Screens/Select/Details/AdvancedStats.cs
+++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs
@@ -15,6 +15,7 @@ using System.Collections.Generic;
 using osu.Game.Rulesets.Mods;
 using System.Linq;
 using System.Threading;
+using osu.Framework.Localisation;
 using osu.Framework.Threading;
 using osu.Framework.Utils;
 using osu.Game.Configuration;
@@ -180,7 +181,7 @@ namespace osu.Game.Screens.Select.Details
             [Resolved]
             private OsuColour colours { get; set; }
 
-            public string Title
+            public LocalisableString Title
             {
                 get => name.Text;
                 set => name.Text = value;
diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs
index 35970cd960..7bdeacc91a 100644
--- a/osu.Game/Screens/Select/FooterButton.cs
+++ b/osu.Game/Screens/Select/FooterButton.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.Containers;
 
@@ -21,9 +22,9 @@ namespace osu.Game.Screens.Select
 
         protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
 
-        public string Text
+        public LocalisableString Text
         {
-            get => SpriteText?.Text;
+            get => SpriteText.Text;
             set
             {
                 if (SpriteText != null)
diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
index 6e2f3cc9df..845c0a914e 100644
--- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
+++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Effects;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osuTK;
@@ -39,13 +40,13 @@ namespace osu.Game.Screens.Select.Options
             set => iconText.Icon = value;
         }
 
-        public string FirstLineText
+        public LocalisableString FirstLineText
         {
             get => firstLine.Text;
             set => firstLine.Text = value;
         }
 
-        public string SecondLineText
+        public LocalisableString SecondLineText
         {
             get => secondLine.Text;
             set => secondLine.Text = value;
diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs
index 567dd348e1..06461127b1 100644
--- a/osu.Game/Skinning/SkinnableSpriteText.cs
+++ b/osu.Game/Skinning/SkinnableSpriteText.cs
@@ -3,6 +3,7 @@
 
 using System;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 
 namespace osu.Game.Skinning
 {
@@ -21,9 +22,9 @@ namespace osu.Game.Skinning
                 textDrawable.Text = Text;
         }
 
-        private string text;
+        private LocalisableString text;
 
-        public string Text
+        public LocalisableString Text
         {
             get => text;
             set

From 8a97e2e28da1f7ad257c3bdf28c98f7c6bfe826f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Feb 2021 17:14:13 +0900
Subject: [PATCH 033/434] Update LocalisedString usages to RomanisedString

---
 osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++--
 .../Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs   | 5 +++--
 .../Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs   | 5 +++--
 .../BeatmapSet/Scores/TopScoreStatisticsSection.cs       | 2 +-
 osu.Game/Overlays/Music/PlaylistItem.cs                  | 4 ++--
 osu.Game/Overlays/NowPlayingOverlay.cs                   | 5 +++--
 .../Sections/Historical/DrawableMostPlayedBeatmap.cs     | 9 +++++----
 .../Profile/Sections/Ranks/DrawableProfileScore.cs       | 9 +++++----
 osu.Game/Overlays/Settings/Sections/SkinSection.cs       | 1 +
 osu.Game/Screens/Menu/SongTicker.cs                      | 5 +++--
 osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs   | 5 +++--
 osu.Game/Screens/Play/BeatmapMetadataDisplay.cs          | 5 +++--
 .../Ranking/Expanded/ExpandedPanelMiddleContent.cs       | 5 +++--
 osu.Game/Screens/Select/BeatmapInfoWedge.cs              | 4 ++--
 osu.Game/Screens/Select/Carousel/SetPanelContent.cs      | 5 +++--
 15 files changed, 42 insertions(+), 31 deletions(-)

diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index d1197b1a61..e6d73c6e83 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -74,9 +74,9 @@ namespace osu.Game.Tournament.Components
                     {
                         new TournamentSpriteText
                         {
-                            Text = new LocalisedString((
+                            Text = new RomanisableString(
                                 $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}",
-                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")),
+                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"),
                             Font = OsuFont.Torus.With(weight: FontWeight.Bold),
                         },
                         new FillFlowContainer
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
index 97e7ce83a5..ba4725b49a 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
@@ -83,14 +84,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
                                     {
                                         new OsuSpriteText
                                         {
-                                            Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)),
+                                            Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode),
                                             Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true)
                                         },
                                     }
                                 },
                                 new OsuSpriteText
                                 {
-                                    Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)),
+                                    Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode),
                                     Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
                                 },
                             },
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
index 4a887ed571..624cb89d1e 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
@@ -106,14 +107,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
                                                             {
                                                                 new OsuSpriteText
                                                                 {
-                                                                    Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)),
+                                                                    Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode),
                                                                     Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true)
                                                                 },
                                                             }
                                                         },
                                                         new OsuSpriteText
                                                         {
-                                                            Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)),
+                                                            Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode),
                                                             Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
                                                         },
                                                     }
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index c281d7b432..5cb834b510 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -203,7 +203,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                 this.text = text;
             }
 
-            public LocalisedString Text
+            public string Text
             {
                 set => text.Text = value;
             }
diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs
index 96dff39fae..dab9bc9629 100644
--- a/osu.Game/Overlays/Music/PlaylistItem.cs
+++ b/osu.Game/Overlays/Music/PlaylistItem.cs
@@ -48,8 +48,8 @@ namespace osu.Game.Overlays.Music
             artistColour = colours.Gray9;
             HandleColour = colours.Gray5;
 
-            title = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title)));
-            artist = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist)));
+            title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Title, Model.Metadata.TitleUnicode));
+            artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Artist, Model.Metadata.ArtistUnicode));
         }
 
         protected override void LoadComplete()
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 74317a143c..9c17392e25 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -292,8 +293,8 @@ namespace osu.Game.Overlays
                     else
                     {
                         BeatmapMetadata metadata = beatmap.Metadata;
-                        title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title));
-                        artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist));
+                        title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode);
+                        artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode);
                     }
                 });
 
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
index e485802095..48a0481b9e 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
@@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osuTK;
 using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
 
 namespace osu.Game.Overlays.Profile.Sections.Historical
 {
@@ -128,14 +129,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
             {
                 new OsuSpriteText
                 {
-                    Text = new LocalisedString((
-                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ",
-                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ")),
+                    Text = new RomanisableString(
+                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ",
+                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] "),
                     Font = OsuFont.GetFont(weight: FontWeight.Bold)
                 },
                 new OsuSpriteText
                 {
-                    Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)),
+                    Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode),
                     Font = OsuFont.GetFont(weight: FontWeight.Regular)
                 },
             };
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index 859637485f..ca9e19cd56 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
@@ -255,16 +256,16 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
                 {
                     Anchor = Anchor.BottomLeft,
                     Origin = Anchor.BottomLeft,
-                    Text = new LocalisedString((
-                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ",
-                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ")),
+                    Text = new RomanisableString(
+                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ",
+                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} "),
                     Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true)
                 },
                 new OsuSpriteText
                 {
                     Anchor = Anchor.BottomLeft,
                     Origin = Anchor.BottomLeft,
-                    Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)),
+                    Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode),
                     Font = OsuFont.GetFont(size: 12, italics: true)
                 },
             };
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 75068bd611..316837d27d 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 using osu.Framework.Logging;
 using osu.Game.Configuration;
 using osu.Game.Graphics.UserInterface;
diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs
index fd9d9a3fac..2be446d71a 100644
--- a/osu.Game/Screens/Menu/SongTicker.cs
+++ b/osu.Game/Screens/Menu/SongTicker.cs
@@ -8,6 +8,7 @@ using osu.Game.Graphics.Sprites;
 using osuTK;
 using osu.Game.Graphics;
 using osu.Framework.Bindables;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 
 namespace osu.Game.Screens.Menu
@@ -60,8 +61,8 @@ namespace osu.Game.Screens.Menu
         {
             var metadata = beatmap.Value.Metadata;
 
-            title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title));
-            artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist));
+            title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode);
+            artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode);
 
             this.FadeInFromZero(fade_duration / 2f)
                 .Delay(4000)
diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
index b64ea37a59..299e3e3768 100644
--- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
@@ -4,6 +4,7 @@
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
+using osu.Framework.Localisation;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
@@ -72,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
                 {
                     new OsuSpriteText
                     {
-                        Text = new LocalisedString((beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist)),
+                        Text = new RomanisableString(beatmap.Value.Metadata.Artist, beatmap.Value.Metadata.ArtistUnicode),
                         Font = OsuFont.GetFont(size: TextSize),
                     },
                     new OsuSpriteText
@@ -82,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
                     },
                     new OsuSpriteText
                     {
-                        Text = new LocalisedString((beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title)),
+                        Text = new RomanisableString(beatmap.Value.Metadata.Title, beatmap.Value.Metadata.TitleUnicode),
                         Font = OsuFont.GetFont(size: TextSize),
                     }
                 }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap");
diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
index bb82b00100..0779a9c637 100644
--- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
+++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
@@ -72,7 +73,7 @@ namespace osu.Game.Screens.Play
                         }),
                         new OsuSpriteText
                         {
-                            Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
+                            Text = new RomanisableString(metadata.Title, metadata.TitleUnicode),
                             Font = OsuFont.GetFont(size: 36, italics: true),
                             Origin = Anchor.TopCentre,
                             Anchor = Anchor.TopCentre,
@@ -80,7 +81,7 @@ namespace osu.Game.Screens.Play
                         },
                         new OsuSpriteText
                         {
-                            Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
+                            Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode),
                             Font = OsuFont.GetFont(size: 26, italics: true),
                             Origin = Anchor.TopCentre,
                             Anchor = Anchor.TopCentre,
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 85a9b06a70..234e4f2023 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -6,6 +6,7 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
@@ -100,7 +101,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                 {
                                     Anchor = Anchor.TopCentre,
                                     Origin = Anchor.TopCentre,
-                                    Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
+                                    Text = new RomanisableString(metadata.Title, metadata.TitleUnicode),
                                     Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
                                     MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
                                     Truncate = true,
@@ -109,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                 {
                                     Anchor = Anchor.TopCentre,
                                     Origin = Anchor.TopCentre,
-                                    Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
+                                    Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode),
                                     Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
                                     MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
                                     Truncate = true,
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 86cb561bc7..0c5b67026c 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -187,8 +187,8 @@ namespace osu.Game.Screens.Select
 
                 RelativeSizeAxes = Axes.Both;
 
-                titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title)));
-                artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist)));
+                titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Title, metadata.TitleUnicode));
+                artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Artist, metadata.ArtistUnicode));
 
                 Children = new Drawable[]
                 {
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index 82704c24fb..0e99a4ce70 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -6,6 +6,7 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
@@ -40,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel
                 {
                     new OsuSpriteText
                     {
-                        Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)),
+                        Text = new RomanisableString(beatmapSet.Metadata.Title, beatmapSet.Metadata.TitleUnicode),
                         Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
                         Shadow = true,
                     },
                     new OsuSpriteText
                     {
-                        Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)),
+                        Text = new RomanisableString(beatmapSet.Metadata.Artist, beatmapSet.Metadata.ArtistUnicode),
                         Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
                         Shadow = true,
                     },

From 5e9040c29108cf423ddcf0d6ea418f4aa6690b46 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 22 Feb 2021 16:26:35 +0300
Subject: [PATCH 034/434] Use "pausing supported" conditional instead

---
 osu.Game/Screens/Play/Player.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 5a86ac646a..0046eea91c 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -427,7 +427,7 @@ namespace osu.Game.Screens.Play
 
         private void updatePauseOnFocusLostState()
         {
-            if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value)
+            if (!PauseOnFocusLost || pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value)
                 return;
 
             if (gameActive.Value == false)

From 5493c55da7287bea7f77651ea0474587cc426626 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 22 Feb 2021 16:59:35 +0300
Subject: [PATCH 035/434] Fix silly mistake

---
 osu.Game/Screens/Play/Player.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 0046eea91c..2ded1752da 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -427,7 +427,7 @@ namespace osu.Game.Screens.Play
 
         private void updatePauseOnFocusLostState()
         {
-            if (!PauseOnFocusLost || pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value)
+            if (!PauseOnFocusLost || !pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value)
                 return;
 
             if (gameActive.Value == false)

From f62120c66b6cbb852f96d2ede5e60b933214f08b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 22 Feb 2021 22:45:55 +0100
Subject: [PATCH 036/434] Remove unused using directive

---
 .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs          | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index e34da1ef0c..be8032cde8 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Net;
-using System.Threading.Tasks;
 using JetBrains.Annotations;
 using Newtonsoft.Json.Linq;
 using NUnit.Framework;

From 6a5c6febc56567cd5acb43fdb274f9918c8ef230 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 13:23:32 +0900
Subject: [PATCH 037/434] Add inline comment explaining the retry loop

---
 osu.Game/Screens/Play/Player.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 2ded1752da..e81efdac78 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -434,6 +434,8 @@ namespace osu.Game.Screens.Play
             {
                 bool paused = Pause();
 
+                // if the initial pause could not be satisfied, the pause cooldown may be active.
+                // reschedule the pause attempt until it can be achieved.
                 if (!paused)
                     Scheduler.AddOnce(updatePauseOnFocusLostState);
             }

From 996c0897d1faa058fa12ea07b13799e01b67aab9 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 13:40:21 +0900
Subject: [PATCH 038/434] Seek via GameplayClockContainer for better
 reliability

---
 osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index e5959a3edf..5d070b424a 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Navigation
 
             AddStep("press enter", () => InputManager.Key(Key.Enter));
             AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
-            AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length));
+            AddStep("seek to end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Track.Length));
             AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
             AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
             AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);

From 672fd3f9d2935099f24ffa5f2a879295c6970f77 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 14:24:24 +0900
Subject: [PATCH 039/434] When disable mouse buttons during gameplay is
 selected, disable more globally

Until now the disable setting would only apply to left/right buttons,
and only in gameplay. This change will cause any global actions bound to
mouse buttons to also not work during gameplay.

Closes #11879.
---
 osu.Game/Rulesets/UI/RulesetInputManager.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 07de2bf601..963c3427d0 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.UI
         {
             switch (e)
             {
-                case MouseDownEvent mouseDown when mouseDown.Button == MouseButton.Left || mouseDown.Button == MouseButton.Right:
+                case MouseDownEvent _:
                     if (mouseDisabled.Value)
-                        return false;
+                        return true; // importantly, block upwards propagation so global bindings also don't fire.
 
                     break;
 

From ec4b770cbac2260bfb731c64d8ba4f7990c9c02a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 14:56:03 +0900
Subject: [PATCH 040/434] Remove unused using statement

---
 osu.Game/Rulesets/UI/RulesetInputManager.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 963c3427d0..d6f002ea2c 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -16,7 +16,6 @@ using osu.Game.Configuration;
 using osu.Game.Input.Bindings;
 using osu.Game.Input.Handlers;
 using osu.Game.Screens.Play;
-using osuTK.Input;
 using static osu.Game.Input.Handlers.ReplayInputHandler;
 
 namespace osu.Game.Rulesets.UI

From 664d243003b3fab7d4d6b008302b95fdf7ac12c0 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 15:22:46 +0900
Subject: [PATCH 041/434] Disable multiplayer/spectator on iOS until it can be
 supported again

---
 osu.Game/Online/API/APIAccess.cs | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 8ffa0221c8..ce01378b17 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -10,6 +10,7 @@ using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Newtonsoft.Json.Linq;
+using osu.Framework;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.ExceptionExtensions;
 using osu.Framework.Extensions.ObjectExtensions;
@@ -246,7 +247,14 @@ namespace osu.Game.Online.API
             this.password = password;
         }
 
-        public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this, versionHash);
+        public IHubClientConnector GetHubConnector(string clientName, string endpoint)
+        {
+            // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
+            if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
+                return null;
+
+            return new HubClientConnector(clientName, endpoint, this, versionHash);
+        }
 
         public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
         {

From c514233141756f9700bb3b51881480ddba98f8ab Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Feb 2021 15:57:41 +0900
Subject: [PATCH 042/434] Fix importing collections twice from stable causing a
 hard crash

Somehow a bindable equality check failure got through review. Not sure
if there's some way to protect against this going forward, but we may
want to.
---
 osu.Game/Collections/CollectionManager.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index a65d9a415d..fb9c230c7a 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -138,10 +138,10 @@ namespace osu.Game.Collections
 
             PostNotification?.Invoke(notification);
 
-            var collection = readCollections(stream, notification);
-            await importCollections(collection);
+            var collections = readCollections(stream, notification);
+            await importCollections(collections);
 
-            notification.CompletionText = $"Imported {collection.Count} collections";
+            notification.CompletionText = $"Imported {collections.Count} collections";
             notification.State = ProgressNotificationState.Completed;
         }
 
@@ -155,7 +155,7 @@ namespace osu.Game.Collections
                 {
                     foreach (var newCol in newCollections)
                     {
-                        var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
+                        var existing = Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value);
                         if (existing == null)
                             Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
 

From f45cedeb8524206a37f7dd7a59cadf5ed20fde21 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Tue, 23 Feb 2021 15:38:09 +0000
Subject: [PATCH 043/434] Adjust initial and final rate ranges and prevent them
 from overlapping

---
 osu.Game/Rulesets/Mods/ModWindDown.cs | 13 +++++++++++--
 osu.Game/Rulesets/Mods/ModWindUp.cs   | 13 +++++++++++--
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index 679b50057b..c47ec5fbde 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
         [SettingSource("Initial rate", "The starting speed of the track")]
         public override BindableNumber<double> InitialRate { get; } = new BindableDouble
         {
-            MinValue = 1,
+            MinValue = 0.5,
             MaxValue = 2,
             Default = 1,
             Value = 1,
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods
         public override BindableNumber<double> FinalRate { get; } = new BindableDouble
         {
             MinValue = 0.5,
-            MaxValue = 0.99,
+            MaxValue = 2,
             Default = 0.75,
             Value = 0.75,
             Precision = 0.01,
@@ -45,5 +45,14 @@ namespace osu.Game.Rulesets.Mods
         };
 
         public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray();
+
+        public ModWindDown()
+        {
+            InitialRate.BindValueChanged(val =>
+                InitialRate.Value = Math.Max(val.NewValue, FinalRate.Value + 0.01));
+
+            FinalRate.BindValueChanged(val =>
+                FinalRate.Value = Math.Min(val.NewValue, InitialRate.Value - 0.01));
+        }
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index b733bf423e..5a0fab5e67 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
         public override BindableNumber<double> InitialRate { get; } = new BindableDouble
         {
             MinValue = 0.5,
-            MaxValue = 1,
+            MaxValue = 2,
             Default = 1,
             Value = 1,
             Precision = 0.01,
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
         [SettingSource("Final rate", "The speed increase to ramp towards")]
         public override BindableNumber<double> FinalRate { get; } = new BindableDouble
         {
-            MinValue = 1.01,
+            MinValue = 0.5,
             MaxValue = 2,
             Default = 1.5,
             Value = 1.5,
@@ -45,5 +45,14 @@ namespace osu.Game.Rulesets.Mods
         };
 
         public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray();
+
+        public ModWindUp()
+        {
+            InitialRate.BindValueChanged(val =>
+                InitialRate.Value = Math.Min(val.NewValue, FinalRate.Value - 0.01));
+
+            FinalRate.BindValueChanged(val =>
+                FinalRate.Value = Math.Max(val.NewValue, InitialRate.Value + 0.01));
+        }
     }
 }

From a6e840634b255ea86e21cfa8140df3794965ebe6 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Tue, 23 Feb 2021 15:52:53 +0000
Subject: [PATCH 044/434] Adjust scrubbing behaviour to allow dragging through
 rate values

---
 osu.Game/Rulesets/Mods/ModWindDown.cs | 8 ++++----
 osu.Game/Rulesets/Mods/ModWindUp.cs   | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index c47ec5fbde..f9e6854dd4 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
         [SettingSource("Initial rate", "The starting speed of the track")]
         public override BindableNumber<double> InitialRate { get; } = new BindableDouble
         {
-            MinValue = 0.5,
+            MinValue = 0.51,
             MaxValue = 2,
             Default = 1,
             Value = 1,
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods
         public override BindableNumber<double> FinalRate { get; } = new BindableDouble
         {
             MinValue = 0.5,
-            MaxValue = 2,
+            MaxValue = 1.99,
             Default = 0.75,
             Value = 0.75,
             Precision = 0.01,
@@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods
         public ModWindDown()
         {
             InitialRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Max(val.NewValue, FinalRate.Value + 0.01));
+                FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - 0.01));
 
             FinalRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Min(val.NewValue, InitialRate.Value - 0.01));
+                InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + 0.01));
         }
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index 5a0fab5e67..0d57bbb52d 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
         public override BindableNumber<double> InitialRate { get; } = new BindableDouble
         {
             MinValue = 0.5,
-            MaxValue = 2,
+            MaxValue = 1.99,
             Default = 1,
             Value = 1,
             Precision = 0.01,
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
         [SettingSource("Final rate", "The speed increase to ramp towards")]
         public override BindableNumber<double> FinalRate { get; } = new BindableDouble
         {
-            MinValue = 0.5,
+            MinValue = 0.51,
             MaxValue = 2,
             Default = 1.5,
             Value = 1.5,
@@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods
         public ModWindUp()
         {
             InitialRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Min(val.NewValue, FinalRate.Value - 0.01));
+                FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + 0.01));
 
             FinalRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Max(val.NewValue, InitialRate.Value + 0.01));
+                InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - 0.01));
         }
     }
 }

From 7394c62cc8ee4c30ce12543fa7c6609d7ee9dc58 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Tue, 23 Feb 2021 18:10:03 +0000
Subject: [PATCH 045/434] Make ModTimeRamp and ModRateAdjust incompatible

---
 osu.Game/Rulesets/Mods/ModRateAdjust.cs | 3 +++
 osu.Game/Rulesets/Mods/ModTimeRamp.cs   | 2 ++
 2 files changed, 5 insertions(+)

diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
index b016a6d43b..e66650f7b4 100644
--- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Bindables;
@@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mods
 
         public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
 
+        public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
+
         public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index 330945d3d3..b5cd64dafa 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods
         [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
         public abstract BindableBool AdjustPitch { get; }
 
+        public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) };
+
         public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
 
         private double finalRateTime;

From dbde47fe94e5c26270e4b124f7539c953f32b5b4 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Tue, 23 Feb 2021 19:43:04 +0000
Subject: [PATCH 046/434] Fix test failure

---
 osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 7a0dd5b719..650ae68ffc 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Gameplay
                     break;
 
                 case ModTimeRamp m:
-                    m.InitialRate.Value = m.FinalRate.Value = expectedRate;
+                    m.FinalRate.Value = m.InitialRate.Value = expectedRate;
                     break;
             }
 

From f6d3cd6413e55eb4f44dc87d66644da55ecb0699 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Tue, 23 Feb 2021 21:25:59 +0000
Subject: [PATCH 047/434] Change SamplePlaybackWithRateMods to use rate
 calulated from the sample Replace hardcoded numbers

---
 osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 9 +++++++--
 osu.Game/Rulesets/Mods/ModWindDown.cs                 | 4 ++--
 osu.Game/Rulesets/Mods/ModWindUp.cs                   | 4 ++--
 3 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 650ae68ffc..10a1a13ba0 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Audio;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.IO.Stores;
 using osu.Framework.Testing;
+using osu.Framework.Utils;
 using osu.Game.Audio;
 using osu.Game.IO;
 using osu.Game.Rulesets;
@@ -90,6 +91,7 @@ namespace osu.Game.Tests.Gameplay
         public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
         {
             GameplayClockContainer gameplayContainer = null;
+            StoryboardSampleInfo sampleInfo = null;
             TestDrawableStoryboardSample sample = null;
 
             Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
@@ -117,7 +119,7 @@ namespace osu.Game.Tests.Gameplay
                     Child = beatmapSkinSourceContainer
                 });
 
-                beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
+                beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1))
                 {
                     Clock = gameplayContainer.GameplayClock
                 });
@@ -125,7 +127,10 @@ namespace osu.Game.Tests.Gameplay
 
             AddStep("start", () => gameplayContainer.Start());
 
-            AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate);
+            AddAssert("sample playback rate matches mod rates", () =>
+                testedMod != null && Precision.AlmostEquals(
+                    sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value,
+                    ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime)));
         }
 
         private class TestSkin : LegacySkin
diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index f9e6854dd4..9bd5b5eefd 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods
         public ModWindDown()
         {
             InitialRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - 0.01));
+                FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - FinalRate.Precision));
 
             FinalRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + 0.01));
+                InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + InitialRate.Precision));
         }
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index 0d57bbb52d..39d3c9c5d5 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods
         public ModWindUp()
         {
             InitialRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + 0.01));
+                FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + FinalRate.Precision));
 
             FinalRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - 0.01));
+                InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - InitialRate.Precision));
         }
     }
 }

From 71182347d677be782005acaf1e227c6cd21a0275 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 11:30:13 +0900
Subject: [PATCH 048/434] Also add a notifiation when trying to enter the
 multiplayer screen

Turns out the only check required to get into this screen was that the
API was online, which it always is even if the multiplayer component
isn't.

This provides a better end-user experience.
---
 osu.Game/Screens/Menu/ButtonSystem.cs | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 81b1cb0bf1..dd1e318aa0 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -172,6 +172,23 @@ namespace osu.Game.Screens.Menu
                 return;
             }
 
+            // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
+            if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
+            {
+                notifications?.Post(new SimpleNotification
+                {
+                    Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.",
+                    Icon = FontAwesome.Solid.AppleAlt,
+                    Activated = () =>
+                    {
+                        loginOverlay?.Show();
+                        return true;
+                    }
+                });
+
+                return;
+            }
+
             OnMultiplayer?.Invoke();
         }
 

From e1f71038e39b09134ae2587692f7a9b9fa884d75 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 12:13:55 +0900
Subject: [PATCH 049/434] Remove unncessary action

---
 osu.Game/Screens/Menu/ButtonSystem.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index dd1e318aa0..f93bfd7705 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -179,11 +179,6 @@ namespace osu.Game.Screens.Menu
                 {
                     Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.",
                     Icon = FontAwesome.Solid.AppleAlt,
-                    Activated = () =>
-                    {
-                        loginOverlay?.Show();
-                        return true;
-                    }
                 });
 
                 return;

From 7000132d034c6cf012b475ec44178c7202ca4c3a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 12:45:00 +0900
Subject: [PATCH 050/434] Specify full filename inline for quick beatmap

---
 osu.Game.Tests/Resources/TestResources.cs | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 14bc2c8733..c979b5c695 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Tests.Resources
 
         public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}");
 
-        public static Stream GetTestBeatmapStream(bool virtualTrack = false, bool quick = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}{(quick ? "_quick" : "")}.osz");
+        public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz");
 
         /// <summary>
         /// Retrieve a path to a copy of a shortened (~10 second) beatmap archive with a virtual track.
@@ -24,8 +24,7 @@ namespace osu.Game.Tests.Resources
         public static string GetQuickTestBeatmapForImport()
         {
             var tempPath = Path.GetTempFileName() + ".osz";
-
-            using (var stream = GetTestBeatmapStream(true, true))
+            using (var stream = OpenResource($"Archives/241526 Soleily - Renatus_virtual_quick.osz"))
             using (var newFile = File.Create(tempPath))
                 stream.CopyTo(newFile);
 

From 59e6bad0b9cf128c4f208a67fface6ad82ff48bd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 12:46:35 +0900
Subject: [PATCH 051/434] Remove unnecessary interpolated string specification

---
 osu.Game.Tests/Resources/TestResources.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index c979b5c695..cef0532f9d 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Resources
         public static string GetQuickTestBeatmapForImport()
         {
             var tempPath = Path.GetTempFileName() + ".osz";
-            using (var stream = OpenResource($"Archives/241526 Soleily - Renatus_virtual_quick.osz"))
+            using (var stream = OpenResource("Archives/241526 Soleily - Renatus_virtual_quick.osz"))
             using (var newFile = File.Create(tempPath))
                 stream.CopyTo(newFile);
 

From dd702ccfd22ef251000985bdb72e71812855893e Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 24 Feb 2021 13:39:15 +0900
Subject: [PATCH 052/434] Make mania FI/HD incompatible with each other

---
 .../Mods/ManiaModFadeIn.cs                    | 10 +++--
 .../Mods/ManiaModHidden.cs                    | 33 +-------------
 .../Mods/ManiaModPlayfieldCover.cs            | 43 +++++++++++++++++++
 3 files changed, 51 insertions(+), 35 deletions(-)
 create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs

diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index cbdcd49c5b..f80c9e1f7c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -1,18 +1,20 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
+using System;
+using System.Linq;
 using osu.Game.Rulesets.Mania.UI;
 
 namespace osu.Game.Rulesets.Mania.Mods
 {
-    public class ManiaModFadeIn : ManiaModHidden
+    public class ManiaModFadeIn : ManiaModPlayfieldCover
     {
         public override string Name => "Fade In";
         public override string Acronym => "FI";
-        public override IconUsage? Icon => OsuIcon.ModHidden;
         public override string Description => @"Keys appear out of nowhere!";
+        public override double ScoreMultiplier => 1;
+
+        public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
 
         protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
     }
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index 4bdb15526f..a68f12cb84 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -3,43 +3,14 @@
 
 using System;
 using System.Linq;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.UI;
 
 namespace osu.Game.Rulesets.Mania.Mods
 {
-    public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject>
+    public class ManiaModHidden : ManiaModPlayfieldCover
     {
         public override string Description => @"Keys fade out before you hit them!";
         public override double ScoreMultiplier => 1;
-        public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
 
-        /// <summary>
-        /// The direction in which the cover should expand.
-        /// </summary>
-        protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
-
-        public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
-        {
-            ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
-
-            foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
-            {
-                HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
-                Container hocParent = (Container)hoc.Parent;
-
-                hocParent.Remove(hoc);
-                hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
-                {
-                    c.RelativeSizeAxes = Axes.Both;
-                    c.Direction = ExpandDirection;
-                    c.Coverage = 0.5f;
-                }));
-            }
-        }
+        public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
new file mode 100644
index 0000000000..78c3331fbf
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Mania.Mods
+{
+    public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset<ManiaHitObject>
+    {
+        public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<ManiaHitObject>) };
+
+        /// <summary>
+        /// The direction in which the cover should expand.
+        /// </summary>
+        protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+
+        public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
+        {
+            ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
+
+            foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
+            {
+                HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
+                Container hocParent = (Container)hoc.Parent;
+
+                hocParent.Remove(hoc);
+                hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
+                {
+                    c.RelativeSizeAxes = Axes.Both;
+                    c.Direction = ExpandDirection;
+                    c.Coverage = 0.5f;
+                }));
+            }
+        }
+    }
+}

From 30a58691f04b48126fb8714331a6d84cf88b6cd6 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 24 Feb 2021 14:32:50 +0900
Subject: [PATCH 053/434] Make SD and PF incompatible with each other

---
 osu.Game/Rulesets/Mods/ModFailCondition.cs | 25 ++++++++++++++++++++++
 osu.Game/Rulesets/Mods/ModPerfect.cs       |  9 +++++++-
 osu.Game/Rulesets/Mods/ModSuddenDeath.cs   | 15 ++++---------
 3 files changed, 37 insertions(+), 12 deletions(-)
 create mode 100644 osu.Game/Rulesets/Mods/ModFailCondition.cs

diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
new file mode 100644
index 0000000000..40a0843e06
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Mods
+{
+    public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
+    {
+        public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
+
+        public bool PerformFail() => true;
+
+        public bool RestartOnFail => true;
+
+        public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
+        {
+            healthProcessor.FailConditions += FailCondition;
+        }
+
+        protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result);
+    }
+}
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index df0fc9c4b6..d0b09b50f2 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -1,6 +1,8 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
+using System.Linq;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Judgements;
@@ -8,13 +10,18 @@ using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Mods
 {
-    public abstract class ModPerfect : ModSuddenDeath
+    public abstract class ModPerfect : ModFailCondition
     {
         public override string Name => "Perfect";
         public override string Acronym => "PF";
         public override IconUsage? Icon => OsuIcon.ModPerfect;
+        public override ModType Type => ModType.DifficultyIncrease;
+        public override bool Ranked => true;
+        public override double ScoreMultiplier => 1;
         public override string Description => "SS or quit.";
 
+        public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
+
         protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
             => result.Type.AffectsAccuracy()
                && result.Type != result.Judgement.MaxResult;
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index ae71041a64..617ae38feb 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Linq;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Judgements;
@@ -9,7 +10,7 @@ using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Mods
 {
-    public abstract class ModSuddenDeath : Mod, IApplicableToHealthProcessor, IApplicableFailOverride
+    public abstract class ModSuddenDeath : ModFailCondition
     {
         public override string Name => "Sudden Death";
         public override string Acronym => "SD";
@@ -18,18 +19,10 @@ namespace osu.Game.Rulesets.Mods
         public override string Description => "Miss and fail.";
         public override double ScoreMultiplier => 1;
         public override bool Ranked => true;
-        public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
 
-        public bool PerformFail() => true;
+        public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
 
-        public bool RestartOnFail => true;
-
-        public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
-        {
-            healthProcessor.FailConditions += FailCondition;
-        }
-
-        protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
+        protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
             => result.Type.AffectsCombo()
                && !result.IsHit;
     }

From 14160b897e238ebcba242f5fa09f6b237066c960 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 24 Feb 2021 14:42:04 +0900
Subject: [PATCH 054/434] Fix references to ModSuddenDeath

---
 osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +-
 osu.Game/Rulesets/Mods/ModAutoplay.cs         | 2 +-
 osu.Game/Rulesets/Mods/ModNoFail.cs           | 2 +-
 osu.Game/Rulesets/Mods/ModRelax.cs            | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 77de0cb45b..aac830801b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
         public override ModType Type => ModType.Automation;
         public override string Description => @"Automatic cursor movement - just follow the rhythm.";
         public override double ScoreMultiplier => 1;
-        public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
+        public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) };
 
         public bool PerformFail() => false;
 
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index d1d23def67..d6e1d46b06 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
 
         public bool RestartOnFail => false;
 
-        public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
+        public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
 
         public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
 
diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs
index b95ec7490e..c0f24e116a 100644
--- a/osu.Game/Rulesets/Mods/ModNoFail.cs
+++ b/osu.Game/Rulesets/Mods/ModNoFail.cs
@@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods
         public override string Description => "You can't fail, no matter what.";
         public override double ScoreMultiplier => 0.5;
         public override bool Ranked => true;
-        public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModAutoplay) };
+        public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) };
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs
index b6fec42f43..e5995ff180 100644
--- a/osu.Game/Rulesets/Mods/ModRelax.cs
+++ b/osu.Game/Rulesets/Mods/ModRelax.cs
@@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mods
         public override IconUsage? Icon => OsuIcon.ModRelax;
         public override ModType Type => ModType.Automation;
         public override double ScoreMultiplier => 1;
-        public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModSuddenDeath) };
+        public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) };
     }
 }

From 0b44d2483b6f02dd415c461ab6d3081e96cd9971 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 24 Feb 2021 15:03:37 +0900
Subject: [PATCH 055/434] Make some properties virtual

I think they were intended to be this way from the beginning.
---
 osu.Game/Rulesets/Mods/ModFailCondition.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index 40a0843e06..c0d7bae2b2 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -11,9 +11,9 @@ namespace osu.Game.Rulesets.Mods
     {
         public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
 
-        public bool PerformFail() => true;
+        public virtual bool PerformFail() => true;
 
-        public bool RestartOnFail => true;
+        public virtual bool RestartOnFail => true;
 
         public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
         {

From 6b6811063b617b3cf5c0e38a6eae95193759dd18 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 24 Feb 2021 15:05:12 +0900
Subject: [PATCH 056/434] Make ExpandDirection abstract

---
 osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs         | 3 +++
 osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index a68f12cb84..e3ac624a6e 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Linq;
+using osu.Game.Rulesets.Mania.UI;
 
 namespace osu.Game.Rulesets.Mania.Mods
 {
@@ -12,5 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
         public override double ScoreMultiplier => 1;
 
         public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
+
+        protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
index 78c3331fbf..87501d07a5 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
         /// <summary>
         /// The direction in which the cover should expand.
         /// </summary>
-        protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+        protected abstract CoverExpandDirection ExpandDirection { get; }
 
         public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
         {

From 165da3204454999cd8497d0f55987d871774b34c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 18:41:42 +0900
Subject: [PATCH 057/434] Fix dropdown crash on collection name collisions

---
 osu.Game/Collections/CollectionFilterMenuItem.cs | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs
index fe79358223..0617996872 100644
--- a/osu.Game/Collections/CollectionFilterMenuItem.cs
+++ b/osu.Game/Collections/CollectionFilterMenuItem.cs
@@ -36,7 +36,19 @@ namespace osu.Game.Collections
         }
 
         public bool Equals(CollectionFilterMenuItem other)
-            => other != null && CollectionName.Value == other.CollectionName.Value;
+        {
+            if (other == null)
+                return false;
+
+            // collections may have the same name, so compare first on reference equality.
+            // this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager.
+            if (Collection != null)
+                return Collection == other.Collection;
+
+            // fallback to name-based comparison.
+            // this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below).
+            return CollectionName.Value == other.CollectionName.Value;
+        }
 
         public override int GetHashCode() => CollectionName.Value.GetHashCode();
     }

From 6e6fb31c050ad03ce1f064fb8077f8df4d0f7027 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 18:42:26 +0900
Subject: [PATCH 058/434] Add test coverage

---
 .../TestSceneManageCollectionsDialog.cs       | 23 ++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index 1655adf811..eca857f9e5 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Collections
             {
                 manager = new CollectionManager(LocalStorage),
                 Content,
-                dialogOverlay = new DialogOverlay()
+                dialogOverlay = new DialogOverlay(),
             });
 
             Dependencies.Cache(manager);
@@ -134,6 +134,27 @@ namespace osu.Game.Tests.Visual.Collections
             assertCollectionName(0, "2");
         }
 
+        [Test]
+        public void TestCollectionNameCollisions()
+        {
+            AddStep("add dropdown", () =>
+            {
+                Add(new CollectionFilterDropdown
+                    {
+                        Anchor = Anchor.TopRight,
+                        Origin = Anchor.TopRight,
+                        RelativeSizeAxes = Axes.X,
+                        Width = 0.4f,
+                    }
+                );
+            });
+            AddStep("add two collections with same name", () => manager.Collections.AddRange(new[]
+            {
+                new BeatmapCollection { Name = { Value = "1" } },
+                new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
+            }));
+        }
+
         [Test]
         public void TestRemoveCollectionViaButton()
         {

From 5dc0aefb2bf36f7ab18e8c41a1643bcc31b05c98 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 19:54:52 +0900
Subject: [PATCH 059/434] Cancel request on leaving results screen

---
 osu.Game/Screens/Ranking/SoloResultsScreen.cs | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 76b549da1a..4c35096910 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Screens.Ranking
 {
     public class SoloResultsScreen : ResultsScreen
     {
+        private GetScoresRequest getScoreRequest;
+
         [Resolved]
         private RulesetStore rulesets { get; set; }
 
@@ -28,9 +30,16 @@ namespace osu.Game.Screens.Ranking
             if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending)
                 return null;
 
-            var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset);
-            req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
-            return req;
+            getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset);
+            getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != this.Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
+            return getScoreRequest;
+        }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+
+            getScoreRequest?.Cancel();
         }
     }
 }

From 9ed8d902f7ca20f47179d5f6387e0c9583b8b320 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 19:57:42 +0900
Subject: [PATCH 060/434] Fix requests being indefinitely queued when user is
 offline

---
 osu.Game/Online/API/APIAccess.cs | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index ce01378b17..569481d491 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -381,7 +381,13 @@ namespace osu.Game.Online.API
 
         public void Queue(APIRequest request)
         {
-            lock (queue) queue.Enqueue(request);
+            lock (queue)
+            {
+                if (state.Value == APIState.Offline)
+                    return;
+
+                queue.Enqueue(request);
+            }
         }
 
         private void flushQueue(bool failOldRequests = true)
@@ -402,8 +408,6 @@ namespace osu.Game.Online.API
 
         public void Logout()
         {
-            flushQueue();
-
             password = null;
             authentication.Clear();
 
@@ -415,6 +419,7 @@ namespace osu.Game.Online.API
             });
 
             state.Value = APIState.Offline;
+            flushQueue();
         }
 
         private static User createGuestUser() => new GuestUser();

From fa6d797adf9860bbde472efffcca6fa77256fb14 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Feb 2021 20:30:17 +0900
Subject: [PATCH 061/434] Remove redundant prefix

---
 osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
index 4c35096910..9bc696948f 100644
--- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking
                 return null;
 
             getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset);
-            getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != this.Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
+            getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets)));
             return getScoreRequest;
         }
 

From 73d6a3687eacd25e26501cc2b9ea061b86512c38 Mon Sep 17 00:00:00 2001
From: Ronnie Moir <7267697+H2n9@users.noreply.github.com>
Date: Wed, 24 Feb 2021 14:40:56 +0000
Subject: [PATCH 062/434] Change rate correction logic to be more explicit

---
 osu.Game/Rulesets/Mods/ModWindDown.cs | 10 ++++++++--
 osu.Game/Rulesets/Mods/ModWindUp.cs   | 10 ++++++++--
 2 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index 9bd5b5eefd..c8d79325a3 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -49,10 +49,16 @@ namespace osu.Game.Rulesets.Mods
         public ModWindDown()
         {
             InitialRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - FinalRate.Precision));
+            {
+                if (val.NewValue <= FinalRate.Value)
+                    FinalRate.Value = val.NewValue - FinalRate.Precision;
+            });
 
             FinalRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + InitialRate.Precision));
+            {
+                if (val.NewValue >= InitialRate.Value)
+                    InitialRate.Value = val.NewValue + FinalRate.Precision;
+            });
         }
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index 39d3c9c5d5..4fc1f61e02 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -49,10 +49,16 @@ namespace osu.Game.Rulesets.Mods
         public ModWindUp()
         {
             InitialRate.BindValueChanged(val =>
-                FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + FinalRate.Precision));
+            {
+                if (val.NewValue >= FinalRate.Value)
+                    FinalRate.Value = val.NewValue + FinalRate.Precision;
+            });
 
             FinalRate.BindValueChanged(val =>
-                InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - InitialRate.Precision));
+            {
+                if (val.NewValue <= InitialRate.Value)
+                    InitialRate.Value = val.NewValue - FinalRate.Precision;
+            });
         }
     }
 }

From 421b7877d4eb9eed06942666c37d60d962d78b27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 24 Feb 2021 19:16:10 +0100
Subject: [PATCH 063/434] Avoid mixing precision across time ramp bindables

Bears no functional difference, it's just a bit less of an eyesore.
---
 osu.Game/Rulesets/Mods/ModWindDown.cs | 2 +-
 osu.Game/Rulesets/Mods/ModWindUp.cs   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index c8d79325a3..08bd44f7bd 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods
             FinalRate.BindValueChanged(val =>
             {
                 if (val.NewValue >= InitialRate.Value)
-                    InitialRate.Value = val.NewValue + FinalRate.Precision;
+                    InitialRate.Value = val.NewValue + InitialRate.Precision;
             });
         }
     }
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index 4fc1f61e02..df8f781148 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods
             FinalRate.BindValueChanged(val =>
             {
                 if (val.NewValue <= InitialRate.Value)
-                    InitialRate.Value = val.NewValue - FinalRate.Precision;
+                    InitialRate.Value = val.NewValue - InitialRate.Precision;
             });
         }
     }

From a362382d381e6128b2eabc55ff7a6717eb1722ef Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:06:21 +0900
Subject: [PATCH 064/434] Add back more correct null checks

---
 osu.Game/Graphics/UserInterface/OsuButton.cs | 2 +-
 osu.Game/Screens/Select/FooterButton.cs      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index d2114134cf..a22c837080 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface
     {
         public LocalisableString Text
         {
-            get => SpriteText.Text;
+            get => SpriteText?.Text ?? default;
             set
             {
                 if (SpriteText != null)
diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs
index 7bdeacc91a..cd7c1c449f 100644
--- a/osu.Game/Screens/Select/FooterButton.cs
+++ b/osu.Game/Screens/Select/FooterButton.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Screens.Select
 
         public LocalisableString Text
         {
-            get => SpriteText.Text;
+            get => SpriteText?.Text ?? default;
             set
             {
                 if (SpriteText != null)

From 63d48f0c7d786ea069da6ac88fcc1d7e053356e0 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:06:29 +0900
Subject: [PATCH 065/434] Fix incorrect unicode/romanised string order

---
 osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index e6d73c6e83..a86699a9b5 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components
                         new TournamentSpriteText
                         {
                             Text = new RomanisableString(
-                                $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}",
-                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"),
+                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}",
+                                $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}"),
                             Font = OsuFont.Torus.With(weight: FontWeight.Bold),
                         },
                         new FillFlowContainer

From 4cdde422280004f4013124ba29a78cf871f52bc0 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:08:01 +0900
Subject: [PATCH 066/434] Remove unnecessary backing field

---
 osu.Game/Overlays/Chat/Selection/ChannelSection.cs | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
index e18302770c..537ac975ac 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
@@ -15,8 +15,6 @@ namespace osu.Game.Overlays.Chat.Selection
 {
     public class ChannelSection : Container, IHasFilterableChildren
     {
-        private readonly OsuSpriteText header;
-
         public readonly FillFlowContainer<ChannelListItem> ChannelFlow;
 
         public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
@@ -41,7 +39,7 @@ namespace osu.Game.Overlays.Chat.Selection
 
             Children = new Drawable[]
             {
-                header = new OsuSpriteText
+                new OsuSpriteText
                 {
                     Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
                     Text = "All Channels".ToUpperInvariant()

From e82eaffaed6097e59262aa6106b4784edcb29157 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:12:59 +0900
Subject: [PATCH 067/434] Flip order back to original for romanisable strings

---
 osu.Game.Tournament/Components/TournamentBeatmapPanel.cs    | 4 ++--
 osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs | 4 ++--
 osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs | 4 ++--
 osu.Game/Overlays/Music/PlaylistItem.cs                     | 4 ++--
 osu.Game/Overlays/NowPlayingOverlay.cs                      | 4 ++--
 .../Sections/Historical/DrawableMostPlayedBeatmap.cs        | 6 +++---
 .../Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs | 6 +++---
 osu.Game/Screens/Menu/SongTicker.cs                         | 4 ++--
 osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs      | 4 ++--
 osu.Game/Screens/Play/BeatmapMetadataDisplay.cs             | 4 ++--
 .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs  | 4 ++--
 osu.Game/Screens/Select/BeatmapInfoWedge.cs                 | 4 ++--
 osu.Game/Screens/Select/Carousel/SetPanelContent.cs         | 4 ++--
 13 files changed, 28 insertions(+), 28 deletions(-)

diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index a86699a9b5..e6d73c6e83 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components
                         new TournamentSpriteText
                         {
                             Text = new RomanisableString(
-                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}",
-                                $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}"),
+                                $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}",
+                                $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"),
                             Font = OsuFont.Torus.With(weight: FontWeight.Bold),
                         },
                         new FillFlowContainer
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
index ba4725b49a..4d5c387c4a 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs
@@ -84,14 +84,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
                                     {
                                         new OsuSpriteText
                                         {
-                                            Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode),
+                                            Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title),
                                             Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true)
                                         },
                                     }
                                 },
                                 new OsuSpriteText
                                 {
-                                    Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode),
+                                    Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist),
                                     Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
                                 },
                             },
diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
index 624cb89d1e..00ffd168c1 100644
--- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
+++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs
@@ -107,14 +107,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
                                                             {
                                                                 new OsuSpriteText
                                                                 {
-                                                                    Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode),
+                                                                    Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title),
                                                                     Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true)
                                                                 },
                                                             }
                                                         },
                                                         new OsuSpriteText
                                                         {
-                                                            Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode),
+                                                            Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist),
                                                             Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
                                                         },
                                                     }
diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs
index dab9bc9629..571b14428e 100644
--- a/osu.Game/Overlays/Music/PlaylistItem.cs
+++ b/osu.Game/Overlays/Music/PlaylistItem.cs
@@ -48,8 +48,8 @@ namespace osu.Game.Overlays.Music
             artistColour = colours.Gray9;
             HandleColour = colours.Gray5;
 
-            title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Title, Model.Metadata.TitleUnicode));
-            artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Artist, Model.Metadata.ArtistUnicode));
+            title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.TitleUnicode, Model.Metadata.Title));
+            artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.ArtistUnicode, Model.Metadata.Artist));
         }
 
         protected override void LoadComplete()
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 9c17392e25..81bf71cdec 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -293,8 +293,8 @@ namespace osu.Game.Overlays
                     else
                     {
                         BeatmapMetadata metadata = beatmap.Metadata;
-                        title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode);
-                        artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode);
+                        title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title);
+                        artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
                     }
                 });
 
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
index 48a0481b9e..20e40569e8 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs
@@ -130,13 +130,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
                 new OsuSpriteText
                 {
                     Text = new RomanisableString(
-                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ",
-                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] "),
+                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ",
+                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "),
                     Font = OsuFont.GetFont(weight: FontWeight.Bold)
                 },
                 new OsuSpriteText
                 {
-                    Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode),
+                    Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist),
                     Font = OsuFont.GetFont(weight: FontWeight.Regular)
                 },
             };
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index ca9e19cd56..713303285a 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -257,15 +257,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
                     Anchor = Anchor.BottomLeft,
                     Origin = Anchor.BottomLeft,
                     Text = new RomanisableString(
-                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ",
-                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} "),
+                        $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ",
+                        $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "),
                     Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true)
                 },
                 new OsuSpriteText
                 {
                     Anchor = Anchor.BottomLeft,
                     Origin = Anchor.BottomLeft,
-                    Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode),
+                    Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist),
                     Font = OsuFont.GetFont(size: 12, italics: true)
                 },
             };
diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs
index 2be446d71a..237fe43168 100644
--- a/osu.Game/Screens/Menu/SongTicker.cs
+++ b/osu.Game/Screens/Menu/SongTicker.cs
@@ -61,8 +61,8 @@ namespace osu.Game.Screens.Menu
         {
             var metadata = beatmap.Value.Metadata;
 
-            title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode);
-            artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode);
+            title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title);
+            artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
 
             this.FadeInFromZero(fade_duration / 2f)
                 .Delay(4000)
diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
index 299e3e3768..e5a5e35897 100644
--- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
                 {
                     new OsuSpriteText
                     {
-                        Text = new RomanisableString(beatmap.Value.Metadata.Artist, beatmap.Value.Metadata.ArtistUnicode),
+                        Text = new RomanisableString(beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist),
                         Font = OsuFont.GetFont(size: TextSize),
                     },
                     new OsuSpriteText
@@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
                     },
                     new OsuSpriteText
                     {
-                        Text = new RomanisableString(beatmap.Value.Metadata.Title, beatmap.Value.Metadata.TitleUnicode),
+                        Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title),
                         Font = OsuFont.GetFont(size: TextSize),
                     }
                 }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap");
diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
index 0779a9c637..c56344a8fb 100644
--- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
+++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Screens.Play
                         }),
                         new OsuSpriteText
                         {
-                            Text = new RomanisableString(metadata.Title, metadata.TitleUnicode),
+                            Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
                             Font = OsuFont.GetFont(size: 36, italics: true),
                             Origin = Anchor.TopCentre,
                             Anchor = Anchor.TopCentre,
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play
                         },
                         new OsuSpriteText
                         {
-                            Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode),
+                            Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
                             Font = OsuFont.GetFont(size: 26, italics: true),
                             Origin = Anchor.TopCentre,
                             Anchor = Anchor.TopCentre,
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 234e4f2023..6a6b39b61c 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                 {
                                     Anchor = Anchor.TopCentre,
                                     Origin = Anchor.TopCentre,
-                                    Text = new RomanisableString(metadata.Title, metadata.TitleUnicode),
+                                    Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
                                     Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
                                     MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
                                     Truncate = true,
@@ -110,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                 {
                                     Anchor = Anchor.TopCentre,
                                     Origin = Anchor.TopCentre,
-                                    Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode),
+                                    Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
                                     Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
                                     MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
                                     Truncate = true,
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 0c5b67026c..1c1623e334 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -187,8 +187,8 @@ namespace osu.Game.Screens.Select
 
                 RelativeSizeAxes = Axes.Both;
 
-                titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Title, metadata.TitleUnicode));
-                artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Artist, metadata.ArtistUnicode));
+                titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
+                artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
 
                 Children = new Drawable[]
                 {
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index 0e99a4ce70..23a02547b2 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -41,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel
                 {
                     new OsuSpriteText
                     {
-                        Text = new RomanisableString(beatmapSet.Metadata.Title, beatmapSet.Metadata.TitleUnicode),
+                        Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title),
                         Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
                         Shadow = true,
                     },
                     new OsuSpriteText
                     {
-                        Text = new RomanisableString(beatmapSet.Metadata.Artist, beatmapSet.Metadata.ArtistUnicode),
+                        Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist),
                         Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
                         Shadow = true,
                     },

From a08a3d44c796bafb3fac49b8612869b3f8131bd8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:51:23 +0900
Subject: [PATCH 068/434] Add failing test coverage for using hotkeys from main
 menu before toolbar displayed

---
 .../Navigation/TestSceneScreenNavigation.cs       | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 5d070b424a..fc49517cdf 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -214,6 +214,21 @@ namespace osu.Game.Tests.Visual.Navigation
             AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
         }
 
+        [Test]
+        public void TestSettingsViaHotkeyFromMainMenu()
+        {
+            AddAssert("toolbar not displayed", () => Game.Toolbar.State.Value == Visibility.Hidden);
+
+            AddStep("press settings hotkey", () =>
+            {
+                InputManager.PressKey(Key.ControlLeft);
+                InputManager.Key(Key.O);
+                InputManager.ReleaseKey(Key.ControlLeft);
+            });
+
+            AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible);
+        }
+
         private void pushEscape() =>
             AddStep("Press escape", () => InputManager.Key(Key.Escape));
 

From 2c8e62ae3589a28fd00dd68a72c20e38f53ca60b Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 14:52:51 +0900
Subject: [PATCH 069/434] Fix toolbar not completing enough of layout to
 propagate hotkeys to buttons before initial display

---
 osu.Game/Overlays/Toolbar/Toolbar.cs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 393e349bd0..0ccb22df3a 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -37,6 +37,15 @@ namespace osu.Game.Overlays.Toolbar
         {
             RelativeSizeAxes = Axes.X;
             Size = new Vector2(1, HEIGHT);
+            AlwaysPresent = true;
+        }
+
+        protected override void UpdateAfterChildren()
+        {
+            base.UpdateAfterChildren();
+
+            // this only needed to be set for the initial LoadComplete/Update, so layout completes and gets buttons in a state they can correctly handle keyboard input for hotkeys.
+            AlwaysPresent = false;
         }
 
         [BackgroundDependencyLoader(true)]

From 154dc03a8c9f3c0eb4b880cc9c25f0baaf0939a0 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 15:31:50 +0900
Subject: [PATCH 070/434] Update analyser package

---
 Directory.Build.props | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Directory.Build.props b/Directory.Build.props
index 2e1873a9ed..53ad973e47 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
   <ItemGroup Label="Code Analysis">
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" />
     <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
-    <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
   </ItemGroup>
   <PropertyGroup Label="Code Analysis">
     <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet>

From 996b6a1e57c639617fa4f6d897b75dd3fbe47845 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 15:38:43 +0900
Subject: [PATCH 071/434] Add Enum.HasFlag to banned symbols

---
 .editorconfig                  | 5 ++++-
 CodeAnalysis/BannedSymbols.txt | 1 +
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/.editorconfig b/.editorconfig
index a5f7795882..0cdf3b92d3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -194,4 +194,7 @@ dotnet_diagnostic.IDE0068.severity = none
 dotnet_diagnostic.IDE0069.severity = none
 
 #Disable operator overloads requiring alternate named methods
-dotnet_diagnostic.CA2225.severity = none
\ No newline at end of file
+dotnet_diagnostic.CA2225.severity = none
+
+# Banned APIs
+dotnet_diagnostic.RS0030.severity = error
\ No newline at end of file
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 47839608c9..60cce39176 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -7,3 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
 M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
 T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
 T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
+M:System.Enum.HasFlagFast(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
\ No newline at end of file

From dff1d80f3943c705cb4dfdb8b67123b5e80e1592 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 15:38:56 +0900
Subject: [PATCH 072/434] Update HasFlag usages to HasFlagFast

---
 osu.Game.Rulesets.Catch/CatchRuleset.cs       | 27 +++++-----
 .../TestSceneNotes.cs                         |  3 +-
 .../Legacy/DistanceObjectPatternGenerator.cs  | 17 ++++---
 .../Legacy/HitObjectPatternGenerator.cs       | 33 ++++++------
 osu.Game.Rulesets.Mania/ManiaRuleset.cs       | 51 ++++++++++---------
 osu.Game.Rulesets.Osu/OsuRuleset.cs           | 35 ++++++-------
 osu.Game.Rulesets.Taiko/TaikoRuleset.cs       | 29 ++++++-----
 .../Beatmaps/Formats/LegacyBeatmapDecoder.cs  |  5 +-
 osu.Game/Graphics/UserInterface/BarGraph.cs   |  9 ++--
 .../Graphics/UserInterface/TwoLayerButton.cs  | 11 ++--
 osu.Game/Overlays/Toolbar/ToolbarButton.cs    |  5 +-
 osu.Game/Replays/Legacy/LegacyReplayFrame.cs  |  9 ++--
 .../Objects/Legacy/ConvertHitObjectParser.cs  | 19 +++----
 .../Drawables/DrawableStoryboardAnimation.cs  |  9 ++--
 .../Drawables/DrawableStoryboardSprite.cs     |  9 ++--
 15 files changed, 143 insertions(+), 128 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 0a817eca0d..f4ddbd3021 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Difficulty;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring;
 using System;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Rulesets.Catch.Skinning.Legacy;
 using osu.Game.Skinning;
 
@@ -50,40 +51,40 @@ namespace osu.Game.Rulesets.Catch
 
         public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
         {
-            if (mods.HasFlag(LegacyMods.Nightcore))
+            if (mods.HasFlagFast(LegacyMods.Nightcore))
                 yield return new CatchModNightcore();
-            else if (mods.HasFlag(LegacyMods.DoubleTime))
+            else if (mods.HasFlagFast(LegacyMods.DoubleTime))
                 yield return new CatchModDoubleTime();
 
-            if (mods.HasFlag(LegacyMods.Perfect))
+            if (mods.HasFlagFast(LegacyMods.Perfect))
                 yield return new CatchModPerfect();
-            else if (mods.HasFlag(LegacyMods.SuddenDeath))
+            else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
                 yield return new CatchModSuddenDeath();
 
-            if (mods.HasFlag(LegacyMods.Cinema))
+            if (mods.HasFlagFast(LegacyMods.Cinema))
                 yield return new CatchModCinema();
-            else if (mods.HasFlag(LegacyMods.Autoplay))
+            else if (mods.HasFlagFast(LegacyMods.Autoplay))
                 yield return new CatchModAutoplay();
 
-            if (mods.HasFlag(LegacyMods.Easy))
+            if (mods.HasFlagFast(LegacyMods.Easy))
                 yield return new CatchModEasy();
 
-            if (mods.HasFlag(LegacyMods.Flashlight))
+            if (mods.HasFlagFast(LegacyMods.Flashlight))
                 yield return new CatchModFlashlight();
 
-            if (mods.HasFlag(LegacyMods.HalfTime))
+            if (mods.HasFlagFast(LegacyMods.HalfTime))
                 yield return new CatchModHalfTime();
 
-            if (mods.HasFlag(LegacyMods.HardRock))
+            if (mods.HasFlagFast(LegacyMods.HardRock))
                 yield return new CatchModHardRock();
 
-            if (mods.HasFlag(LegacyMods.Hidden))
+            if (mods.HasFlagFast(LegacyMods.Hidden))
                 yield return new CatchModHidden();
 
-            if (mods.HasFlag(LegacyMods.NoFail))
+            if (mods.HasFlagFast(LegacyMods.NoFail))
                 yield return new CatchModNoFail();
 
-            if (mods.HasFlag(LegacyMods.Relax))
+            if (mods.HasFlagFast(LegacyMods.Relax))
                 yield return new CatchModRelax();
         }
 
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index 6b8f5d5d9d..706268e478 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
@@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Tests
         }
 
         private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
-            => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
+            => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor);
 
         private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
             => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 30d33de06e..c81710ed18 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Audio;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mania.MathUtils;
@@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             if (ConversionDifficulty > 6.5)
             {
-                if (convertType.HasFlag(PatternType.LowProbability))
+                if (convertType.HasFlagFast(PatternType.LowProbability))
                     return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
 
                 return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
@@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             if (ConversionDifficulty > 4)
             {
-                if (convertType.HasFlag(PatternType.LowProbability))
+                if (convertType.HasFlagFast(PatternType.LowProbability))
                     return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
 
                 return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
@@ -157,13 +158,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             if (ConversionDifficulty > 2.5)
             {
-                if (convertType.HasFlag(PatternType.LowProbability))
+                if (convertType.HasFlagFast(PatternType.LowProbability))
                     return generateNRandomNotes(StartTime, 0.3, 0, 0);
 
                 return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
             }
 
-            if (convertType.HasFlag(PatternType.LowProbability))
+            if (convertType.HasFlagFast(PatternType.LowProbability))
                 return generateNRandomNotes(StartTime, 0.17, 0, 0);
 
             return generateNRandomNotes(StartTime, 0.27, 0, 0);
@@ -221,7 +222,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
             var pattern = new Pattern();
 
             int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
-            if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+            if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
                 nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
 
             int lastColumn = nextColumn;
@@ -373,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
 
-            bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
+            bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability);
             canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
 
             if (canGenerateTwoNotes)
@@ -406,7 +407,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
             int endTime = startTime + SegmentDuration * SpanCount;
 
             int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
-            if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+            if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
                 nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
 
             for (int i = 0; i < columnRepeat; i++)
@@ -435,7 +436,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
             var pattern = new Pattern();
 
             int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
-            if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
+            if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
                 holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
 
             // Create the hold note
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index bc4ab55767..8e9020ee13 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 using osuTK;
 using osu.Game.Audio;
 using osu.Game.Beatmaps;
@@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
             else
                 convertType |= PatternType.LowProbability;
 
-            if (!convertType.HasFlag(PatternType.KeepSingle))
+            if (!convertType.HasFlagFast(PatternType.KeepSingle))
             {
                 if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8)
                     convertType |= PatternType.Mirror;
@@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
                 int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
 
-                if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
+                if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
                 {
                     // Generate a new pattern by copying the last hit objects in reverse-column order
                     for (int i = RandomStart; i < TotalColumns; i++)
@@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                     return pattern;
                 }
 
-                if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
+                if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
                                                            // If we convert to 7K + 1, let's not overload the special key
                                                            && (TotalColumns != 8 || lastColumn != 0)
                                                            // Make sure the last column was not the centre column
@@ -126,7 +127,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                     return pattern;
                 }
 
-                if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
+                if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
                 {
                     // Generate a new pattern by placing on the already filled columns
                     for (int i = RandomStart; i < TotalColumns; i++)
@@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
                 if (PreviousPattern.HitObjects.Count() == 1)
                 {
-                    if (convertType.HasFlag(PatternType.Stair))
+                    if (convertType.HasFlagFast(PatternType.Stair))
                     {
                         // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
                         int targetColumn = lastColumn + 1;
@@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                         return pattern;
                     }
 
-                    if (convertType.HasFlag(PatternType.ReverseStair))
+                    if (convertType.HasFlagFast(PatternType.ReverseStair))
                     {
                         // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
                         int targetColumn = lastColumn - 1;
@@ -163,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                     }
                 }
 
-                if (convertType.HasFlag(PatternType.KeepSingle))
+                if (convertType.HasFlagFast(PatternType.KeepSingle))
                     return generateRandomNotes(1);
 
-                if (convertType.HasFlag(PatternType.Mirror))
+                if (convertType.HasFlagFast(PatternType.Mirror))
                 {
                     if (ConversionDifficulty > 6.5)
                         return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
@@ -178,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
                 if (ConversionDifficulty > 6.5)
                 {
-                    if (convertType.HasFlag(PatternType.LowProbability))
+                    if (convertType.HasFlagFast(PatternType.LowProbability))
                         return generateRandomPattern(0.78, 0.42, 0, 0);
 
                     return generateRandomPattern(1, 0.62, 0, 0);
@@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
                 if (ConversionDifficulty > 4)
                 {
-                    if (convertType.HasFlag(PatternType.LowProbability))
+                    if (convertType.HasFlagFast(PatternType.LowProbability))
                         return generateRandomPattern(0.35, 0.08, 0, 0);
 
                     return generateRandomPattern(0.52, 0.15, 0, 0);
@@ -194,7 +195,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
                 if (ConversionDifficulty > 2)
                 {
-                    if (convertType.HasFlag(PatternType.LowProbability))
+                    if (convertType.HasFlagFast(PatternType.LowProbability))
                         return generateRandomPattern(0.18, 0, 0, 0);
 
                     return generateRandomPattern(0.45, 0, 0, 0);
@@ -207,9 +208,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             foreach (var obj in p.HitObjects)
             {
-                if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
+                if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1)
                     StairType = PatternType.ReverseStair;
-                if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
+                if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart)
                     StairType = PatternType.Stair;
             }
 
@@ -229,7 +230,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
         {
             var pattern = new Pattern();
 
-            bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
+            bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack);
 
             if (!allowStacking)
                 noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
             int getNextColumn(int last)
             {
-                if (convertType.HasFlag(PatternType.Gathered))
+                if (convertType.HasFlagFast(PatternType.Gathered))
                 {
                     last++;
                     if (last == TotalColumns)
@@ -296,7 +297,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
         /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
         private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
         {
-            if (convertType.HasFlag(PatternType.ForceNotStack))
+            if (convertType.HasFlagFast(PatternType.ForceNotStack))
                 return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
 
             var pattern = new Pattern();
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 4c729fef83..d624e094ad 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.UI;
 using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input.Bindings;
@@ -59,76 +60,76 @@ namespace osu.Game.Rulesets.Mania
 
         public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
         {
-            if (mods.HasFlag(LegacyMods.Nightcore))
+            if (mods.HasFlagFast(LegacyMods.Nightcore))
                 yield return new ManiaModNightcore();
-            else if (mods.HasFlag(LegacyMods.DoubleTime))
+            else if (mods.HasFlagFast(LegacyMods.DoubleTime))
                 yield return new ManiaModDoubleTime();
 
-            if (mods.HasFlag(LegacyMods.Perfect))
+            if (mods.HasFlagFast(LegacyMods.Perfect))
                 yield return new ManiaModPerfect();
-            else if (mods.HasFlag(LegacyMods.SuddenDeath))
+            else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
                 yield return new ManiaModSuddenDeath();
 
-            if (mods.HasFlag(LegacyMods.Cinema))
+            if (mods.HasFlagFast(LegacyMods.Cinema))
                 yield return new ManiaModCinema();
-            else if (mods.HasFlag(LegacyMods.Autoplay))
+            else if (mods.HasFlagFast(LegacyMods.Autoplay))
                 yield return new ManiaModAutoplay();
 
-            if (mods.HasFlag(LegacyMods.Easy))
+            if (mods.HasFlagFast(LegacyMods.Easy))
                 yield return new ManiaModEasy();
 
-            if (mods.HasFlag(LegacyMods.FadeIn))
+            if (mods.HasFlagFast(LegacyMods.FadeIn))
                 yield return new ManiaModFadeIn();
 
-            if (mods.HasFlag(LegacyMods.Flashlight))
+            if (mods.HasFlagFast(LegacyMods.Flashlight))
                 yield return new ManiaModFlashlight();
 
-            if (mods.HasFlag(LegacyMods.HalfTime))
+            if (mods.HasFlagFast(LegacyMods.HalfTime))
                 yield return new ManiaModHalfTime();
 
-            if (mods.HasFlag(LegacyMods.HardRock))
+            if (mods.HasFlagFast(LegacyMods.HardRock))
                 yield return new ManiaModHardRock();
 
-            if (mods.HasFlag(LegacyMods.Hidden))
+            if (mods.HasFlagFast(LegacyMods.Hidden))
                 yield return new ManiaModHidden();
 
-            if (mods.HasFlag(LegacyMods.Key1))
+            if (mods.HasFlagFast(LegacyMods.Key1))
                 yield return new ManiaModKey1();
 
-            if (mods.HasFlag(LegacyMods.Key2))
+            if (mods.HasFlagFast(LegacyMods.Key2))
                 yield return new ManiaModKey2();
 
-            if (mods.HasFlag(LegacyMods.Key3))
+            if (mods.HasFlagFast(LegacyMods.Key3))
                 yield return new ManiaModKey3();
 
-            if (mods.HasFlag(LegacyMods.Key4))
+            if (mods.HasFlagFast(LegacyMods.Key4))
                 yield return new ManiaModKey4();
 
-            if (mods.HasFlag(LegacyMods.Key5))
+            if (mods.HasFlagFast(LegacyMods.Key5))
                 yield return new ManiaModKey5();
 
-            if (mods.HasFlag(LegacyMods.Key6))
+            if (mods.HasFlagFast(LegacyMods.Key6))
                 yield return new ManiaModKey6();
 
-            if (mods.HasFlag(LegacyMods.Key7))
+            if (mods.HasFlagFast(LegacyMods.Key7))
                 yield return new ManiaModKey7();
 
-            if (mods.HasFlag(LegacyMods.Key8))
+            if (mods.HasFlagFast(LegacyMods.Key8))
                 yield return new ManiaModKey8();
 
-            if (mods.HasFlag(LegacyMods.Key9))
+            if (mods.HasFlagFast(LegacyMods.Key9))
                 yield return new ManiaModKey9();
 
-            if (mods.HasFlag(LegacyMods.KeyCoop))
+            if (mods.HasFlagFast(LegacyMods.KeyCoop))
                 yield return new ManiaModDualStages();
 
-            if (mods.HasFlag(LegacyMods.NoFail))
+            if (mods.HasFlagFast(LegacyMods.NoFail))
                 yield return new ManiaModNoFail();
 
-            if (mods.HasFlag(LegacyMods.Random))
+            if (mods.HasFlagFast(LegacyMods.Random))
                 yield return new ManiaModRandom();
 
-            if (mods.HasFlag(LegacyMods.Mirror))
+            if (mods.HasFlagFast(LegacyMods.Mirror))
                 yield return new ManiaModMirror();
         }
 
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 18324a18a8..838d707d64 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -29,6 +29,7 @@ using osu.Game.Scoring;
 using osu.Game.Skinning;
 using System;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.Skinning.Legacy;
 using osu.Game.Rulesets.Osu.Statistics;
@@ -58,52 +59,52 @@ namespace osu.Game.Rulesets.Osu
 
         public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
         {
-            if (mods.HasFlag(LegacyMods.Nightcore))
+            if (mods.HasFlagFast(LegacyMods.Nightcore))
                 yield return new OsuModNightcore();
-            else if (mods.HasFlag(LegacyMods.DoubleTime))
+            else if (mods.HasFlagFast(LegacyMods.DoubleTime))
                 yield return new OsuModDoubleTime();
 
-            if (mods.HasFlag(LegacyMods.Perfect))
+            if (mods.HasFlagFast(LegacyMods.Perfect))
                 yield return new OsuModPerfect();
-            else if (mods.HasFlag(LegacyMods.SuddenDeath))
+            else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
                 yield return new OsuModSuddenDeath();
 
-            if (mods.HasFlag(LegacyMods.Autopilot))
+            if (mods.HasFlagFast(LegacyMods.Autopilot))
                 yield return new OsuModAutopilot();
 
-            if (mods.HasFlag(LegacyMods.Cinema))
+            if (mods.HasFlagFast(LegacyMods.Cinema))
                 yield return new OsuModCinema();
-            else if (mods.HasFlag(LegacyMods.Autoplay))
+            else if (mods.HasFlagFast(LegacyMods.Autoplay))
                 yield return new OsuModAutoplay();
 
-            if (mods.HasFlag(LegacyMods.Easy))
+            if (mods.HasFlagFast(LegacyMods.Easy))
                 yield return new OsuModEasy();
 
-            if (mods.HasFlag(LegacyMods.Flashlight))
+            if (mods.HasFlagFast(LegacyMods.Flashlight))
                 yield return new OsuModFlashlight();
 
-            if (mods.HasFlag(LegacyMods.HalfTime))
+            if (mods.HasFlagFast(LegacyMods.HalfTime))
                 yield return new OsuModHalfTime();
 
-            if (mods.HasFlag(LegacyMods.HardRock))
+            if (mods.HasFlagFast(LegacyMods.HardRock))
                 yield return new OsuModHardRock();
 
-            if (mods.HasFlag(LegacyMods.Hidden))
+            if (mods.HasFlagFast(LegacyMods.Hidden))
                 yield return new OsuModHidden();
 
-            if (mods.HasFlag(LegacyMods.NoFail))
+            if (mods.HasFlagFast(LegacyMods.NoFail))
                 yield return new OsuModNoFail();
 
-            if (mods.HasFlag(LegacyMods.Relax))
+            if (mods.HasFlagFast(LegacyMods.Relax))
                 yield return new OsuModRelax();
 
-            if (mods.HasFlag(LegacyMods.SpunOut))
+            if (mods.HasFlagFast(LegacyMods.SpunOut))
                 yield return new OsuModSpunOut();
 
-            if (mods.HasFlag(LegacyMods.Target))
+            if (mods.HasFlagFast(LegacyMods.Target))
                 yield return new OsuModTarget();
 
-            if (mods.HasFlag(LegacyMods.TouchDevice))
+            if (mods.HasFlagFast(LegacyMods.TouchDevice))
                 yield return new OsuModTouchDevice();
         }
 
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index f2b5d195b4..56f58f404b 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.Taiko.Scoring;
 using osu.Game.Scoring;
 using System;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Taiko.Edit;
 using osu.Game.Rulesets.Taiko.Objects;
@@ -57,43 +58,43 @@ namespace osu.Game.Rulesets.Taiko
 
         public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
         {
-            if (mods.HasFlag(LegacyMods.Nightcore))
+            if (mods.HasFlagFast(LegacyMods.Nightcore))
                 yield return new TaikoModNightcore();
-            else if (mods.HasFlag(LegacyMods.DoubleTime))
+            else if (mods.HasFlagFast(LegacyMods.DoubleTime))
                 yield return new TaikoModDoubleTime();
 
-            if (mods.HasFlag(LegacyMods.Perfect))
+            if (mods.HasFlagFast(LegacyMods.Perfect))
                 yield return new TaikoModPerfect();
-            else if (mods.HasFlag(LegacyMods.SuddenDeath))
+            else if (mods.HasFlagFast(LegacyMods.SuddenDeath))
                 yield return new TaikoModSuddenDeath();
 
-            if (mods.HasFlag(LegacyMods.Cinema))
+            if (mods.HasFlagFast(LegacyMods.Cinema))
                 yield return new TaikoModCinema();
-            else if (mods.HasFlag(LegacyMods.Autoplay))
+            else if (mods.HasFlagFast(LegacyMods.Autoplay))
                 yield return new TaikoModAutoplay();
 
-            if (mods.HasFlag(LegacyMods.Easy))
+            if (mods.HasFlagFast(LegacyMods.Easy))
                 yield return new TaikoModEasy();
 
-            if (mods.HasFlag(LegacyMods.Flashlight))
+            if (mods.HasFlagFast(LegacyMods.Flashlight))
                 yield return new TaikoModFlashlight();
 
-            if (mods.HasFlag(LegacyMods.HalfTime))
+            if (mods.HasFlagFast(LegacyMods.HalfTime))
                 yield return new TaikoModHalfTime();
 
-            if (mods.HasFlag(LegacyMods.HardRock))
+            if (mods.HasFlagFast(LegacyMods.HardRock))
                 yield return new TaikoModHardRock();
 
-            if (mods.HasFlag(LegacyMods.Hidden))
+            if (mods.HasFlagFast(LegacyMods.Hidden))
                 yield return new TaikoModHidden();
 
-            if (mods.HasFlag(LegacyMods.NoFail))
+            if (mods.HasFlagFast(LegacyMods.NoFail))
                 yield return new TaikoModNoFail();
 
-            if (mods.HasFlag(LegacyMods.Relax))
+            if (mods.HasFlagFast(LegacyMods.Relax))
                 yield return new TaikoModRelax();
 
-            if (mods.HasFlag(LegacyMods.Random))
+            if (mods.HasFlagFast(LegacyMods.Random))
                 yield return new TaikoModRandom();
         }
 
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 37ab489da5..99dffa7041 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using osu.Framework.Extensions;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Beatmaps.Legacy;
 using osu.Game.Beatmaps.Timing;
@@ -348,8 +349,8 @@ namespace osu.Game.Beatmaps.Formats
             if (split.Length >= 8)
             {
                 LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]);
-                kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai);
-                omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine);
+                kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai);
+                omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine);
             }
 
             string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs
index 953f3985f9..407bf6a923 100644
--- a/osu.Game/Graphics/UserInterface/BarGraph.cs
+++ b/osu.Game/Graphics/UserInterface/BarGraph.cs
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Extensions.EnumExtensions;
 
 namespace osu.Game.Graphics.UserInterface
 {
@@ -24,11 +25,11 @@ namespace osu.Game.Graphics.UserInterface
             set
             {
                 direction = value;
-                base.Direction = direction.HasFlag(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal;
+                base.Direction = direction.HasFlagFast(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal;
 
                 foreach (var bar in Children)
                 {
-                    bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1);
+                    bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1);
                     bar.Direction = direction;
                 }
             }
@@ -56,14 +57,14 @@ namespace osu.Game.Graphics.UserInterface
                     if (bar.Bar != null)
                     {
                         bar.Bar.Length = length;
-                        bar.Bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1);
+                        bar.Bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1);
                     }
                     else
                     {
                         Add(new Bar
                         {
                             RelativeSizeAxes = Axes.Both,
-                            Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1),
+                            Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1),
                             Length = length,
                             Direction = Direction,
                         });
diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
index 120149d8c1..8f03c7073c 100644
--- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
+++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs
@@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Framework.Audio.Track;
 using System;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics.Effects;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Input.Events;
@@ -56,15 +57,15 @@ namespace osu.Game.Graphics.UserInterface
             set
             {
                 base.Origin = value;
-                c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
-                c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft;
+                c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
+                c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft;
 
-                X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0;
+                X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0;
 
                 Remove(c1);
                 Remove(c2);
-                c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1;
-                c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0;
+                c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1;
+                c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0;
                 Add(c1);
                 Add(c2);
             }
diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
index 83f2bdf6cb..5939f7a42f 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs
@@ -4,6 +4,7 @@
 using osu.Framework.Allocation;
 using osu.Framework.Caching;
 using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Effects;
@@ -127,9 +128,9 @@ namespace osu.Game.Overlays.Toolbar
                 {
                     Direction = FillDirection.Vertical,
                     RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize
-                    Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight,
+                    Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight,
                     Origin = TooltipAnchor,
-                    Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5),
+                    Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5),
                     Alpha = 0,
                     Children = new Drawable[]
                     {
diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
index ab9ccda9b9..f6abf259e8 100644
--- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
+++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
@@ -3,6 +3,7 @@
 
 using MessagePack;
 using Newtonsoft.Json;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Game.Rulesets.Replays;
 using osuTK;
 
@@ -31,19 +32,19 @@ namespace osu.Game.Replays.Legacy
 
         [JsonIgnore]
         [IgnoreMember]
-        public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1);
+        public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1);
 
         [JsonIgnore]
         [IgnoreMember]
-        public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1);
+        public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1);
 
         [JsonIgnore]
         [IgnoreMember]
-        public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2);
+        public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2);
 
         [JsonIgnore]
         [IgnoreMember]
-        public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2);
+        public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
 
         [Key(3)]
         public ReplayButtonState ButtonState;
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 72025de131..8419dd66de 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Formats;
 using osu.Game.Audio;
 using System.Linq;
 using JetBrains.Annotations;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Utils;
 using osu.Game.Beatmaps.Legacy;
 using osu.Game.Skinning;
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
             int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4;
             type &= ~LegacyHitObjectType.ComboOffset;
 
-            bool combo = type.HasFlag(LegacyHitObjectType.NewCombo);
+            bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo);
             type &= ~LegacyHitObjectType.NewCombo;
 
             var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]);
@@ -62,14 +63,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
 
             HitObject result = null;
 
-            if (type.HasFlag(LegacyHitObjectType.Circle))
+            if (type.HasFlagFast(LegacyHitObjectType.Circle))
             {
                 result = CreateHit(pos, combo, comboOffset);
 
                 if (split.Length > 5)
                     readCustomSampleBanks(split[5], bankInfo);
             }
-            else if (type.HasFlag(LegacyHitObjectType.Slider))
+            else if (type.HasFlagFast(LegacyHitObjectType.Slider))
             {
                 double? length = null;
 
@@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
 
                 result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
             }
-            else if (type.HasFlag(LegacyHitObjectType.Spinner))
+            else if (type.HasFlagFast(LegacyHitObjectType.Spinner))
             {
                 double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
 
@@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
                 if (split.Length > 6)
                     readCustomSampleBanks(split[6], bankInfo);
             }
-            else if (type.HasFlag(LegacyHitObjectType.Hold))
+            else if (type.HasFlagFast(LegacyHitObjectType.Hold))
             {
                 // Note: Hold is generated by BMS converts
 
@@ -436,16 +437,16 @@ namespace osu.Game.Rulesets.Objects.Legacy
                 new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank,
                     // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
                     // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
-                    type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal))
+                    type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
             };
 
-            if (type.HasFlag(LegacyHitSoundType.Finish))
+            if (type.HasFlagFast(LegacyHitSoundType.Finish))
                 soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
 
-            if (type.HasFlag(LegacyHitSoundType.Whistle))
+            if (type.HasFlagFast(LegacyHitSoundType.Whistle))
                 soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
 
-            if (type.HasFlag(LegacyHitSoundType.Clap))
+            if (type.HasFlagFast(LegacyHitSoundType.Clap))
                 soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
 
             return soundTypes;
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
index 7eac994e07..81623a9307 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
@@ -3,6 +3,7 @@
 
 using System;
 using osu.Framework.Allocation;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Animations;
 using osu.Framework.Graphics.Textures;
@@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables
 
                 if (FlipH)
                 {
-                    if (origin.HasFlag(Anchor.x0))
+                    if (origin.HasFlagFast(Anchor.x0))
                         origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
-                    else if (origin.HasFlag(Anchor.x2))
+                    else if (origin.HasFlagFast(Anchor.x2))
                         origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
                 }
 
                 if (FlipV)
                 {
-                    if (origin.HasFlag(Anchor.y0))
+                    if (origin.HasFlagFast(Anchor.y0))
                         origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
-                    else if (origin.HasFlag(Anchor.y2))
+                    else if (origin.HasFlagFast(Anchor.y2))
                         origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
                 }
 
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
index 7b1a6d54da..eb877f3dff 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs
@@ -3,6 +3,7 @@
 
 using System;
 using osu.Framework.Allocation;
+using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Textures;
@@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables
 
                 if (FlipH)
                 {
-                    if (origin.HasFlag(Anchor.x0))
+                    if (origin.HasFlagFast(Anchor.x0))
                         origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
-                    else if (origin.HasFlag(Anchor.x2))
+                    else if (origin.HasFlagFast(Anchor.x2))
                         origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2));
                 }
 
                 if (FlipV)
                 {
-                    if (origin.HasFlag(Anchor.y0))
+                    if (origin.HasFlagFast(Anchor.y0))
                         origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
-                    else if (origin.HasFlag(Anchor.y2))
+                    else if (origin.HasFlagFast(Anchor.y2))
                         origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2));
                 }
 

From 9f3ceb99eba64a20a600474963b84ac35b256147 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 16:05:08 +0900
Subject: [PATCH 073/434] Fix the star rating display at song select flashing
 to zero when changing mods

Due to the use of bindable flow provided by `BeatmapDifficultyCache` in
this usage, the display would briefly flash to zero while difficulty
calculation was still running (as there is no way for a consumer of the
provided bindable to know whether the returned 0 is an actual 0 SR or a
"pending" calculation).

While I hope to fix this by making the bindable flow return nullable
values, I think this particular use case works better with non-bindable
flow so have switched across to that.
---
 osu.Game/Screens/Select/Details/AdvancedStats.cs | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs
index ab4f3f4796..0c2cce0bb1 100644
--- a/osu.Game/Screens/Select/Details/AdvancedStats.cs
+++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs
@@ -15,6 +15,7 @@ using System.Collections.Generic;
 using osu.Game.Rulesets.Mods;
 using System.Linq;
 using System.Threading;
+using System.Threading.Tasks;
 using osu.Framework.Threading;
 using osu.Framework.Utils;
 using osu.Game.Configuration;
@@ -137,8 +138,6 @@ namespace osu.Game.Screens.Select.Details
             updateStarDifficulty();
         }
 
-        private IBindable<StarDifficulty> normalStarDifficulty;
-        private IBindable<StarDifficulty> moddedStarDifficulty;
         private CancellationTokenSource starDifficultyCancellationSource;
 
         private void updateStarDifficulty()
@@ -150,13 +149,13 @@ namespace osu.Game.Screens.Select.Details
 
             starDifficultyCancellationSource = new CancellationTokenSource();
 
-            normalStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
-            moddedStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
+            var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
+            var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
 
-            normalStarDifficulty.BindValueChanged(_ => updateDisplay());
-            moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true);
-
-            void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars);
+            Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() =>
+            {
+                starDifficulty.Value = ((float)normalStarDifficulty.Result.Stars, (float)moddedStarDifficulty.Result.Stars);
+            }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
         }
 
         protected override void Dispose(bool isDisposing)

From dcda7f62dff49a69cd1c0fdc8e38c937a490c02b Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 16:10:27 +0900
Subject: [PATCH 074/434] Fix incorrect banned symbol

---
 CodeAnalysis/BannedSymbols.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 60cce39176..46c50dbfa2 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -7,4 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
 M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
 T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
 T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
-M:System.Enum.HasFlagFast(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
\ No newline at end of file
+M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
\ No newline at end of file

From 03771ce8ecedc5adcf405e28f7b07531f5da8f1c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 16:19:01 +0900
Subject: [PATCH 075/434] Allow determining a BeatmapDifficultyCache's bindable
 return's completion state via nullability

---
 osu.Game/Beatmaps/BeatmapDifficultyCache.cs               | 8 ++++----
 osu.Game/Beatmaps/Drawables/DifficultyIcon.cs             | 8 ++++++--
 osu.Game/Scoring/ScoreManager.cs                          | 8 ++++++--
 osu.Game/Screens/Select/BeatmapInfoWedge.cs               | 4 ++--
 .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs    | 7 +++++--
 5 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index 37d262abe5..72a9b36c6f 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Beatmaps
         /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
         /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
         /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
-        public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
+        public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
         {
             var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
 
@@ -91,8 +91,8 @@ namespace osu.Game.Beatmaps
         /// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
         /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
         /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
-        public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
-                                                               CancellationToken cancellationToken = default)
+        public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
+                                                                CancellationToken cancellationToken = default)
             => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
 
         /// <summary>
@@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps
             }
         }
 
-        private class BindableStarDifficulty : Bindable<StarDifficulty>
+        private class BindableStarDifficulty : Bindable<StarDifficulty?>
         {
             public readonly BeatmapInfo Beatmap;
             public readonly CancellationToken CancellationToken;
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
index 96e18f120a..c62b803d1a 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
@@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables
                 this.mods = mods;
             }
 
-            private IBindable<StarDifficulty> localStarDifficulty;
+            private IBindable<StarDifficulty?> localStarDifficulty;
 
             [BackgroundDependencyLoader]
             private void load()
@@ -160,7 +160,11 @@ namespace osu.Game.Beatmaps.Drawables
                 localStarDifficulty = ruleset != null
                     ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token)
                     : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token);
-                localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue);
+                localStarDifficulty.BindValueChanged(d =>
+                {
+                    if (d.NewValue is StarDifficulty diff)
+                        StarDifficulty.Value = diff;
+                });
             }
 
             protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index a6beb19876..96ec9644b5 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -137,7 +137,7 @@ namespace osu.Game.Scoring
                 ScoringMode.BindValueChanged(onScoringModeChanged, true);
             }
 
-            private IBindable<StarDifficulty> difficultyBindable;
+            private IBindable<StarDifficulty?> difficultyBindable;
             private CancellationTokenSource difficultyCancellationSource;
 
             private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
@@ -168,7 +168,11 @@ namespace osu.Game.Scoring
 
                         // We can compute the max combo locally after the async beatmap difficulty computation.
                         difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
-                        difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
+                        difficultyBindable.BindValueChanged(d =>
+                        {
+                            if (d.NewValue is StarDifficulty diff)
+                                updateScore(diff.MaxCombo);
+                        }, true);
 
                         return;
                     }
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 86cb561bc7..3b3ed88ccb 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Screens.Select
         [Resolved]
         private BeatmapDifficultyCache difficultyCache { get; set; }
 
-        private IBindable<StarDifficulty> beatmapDifficulty;
+        private IBindable<StarDifficulty?> beatmapDifficulty;
 
         protected BufferedWedgeInfo Info;
 
@@ -132,7 +132,7 @@ namespace osu.Game.Screens.Select
                     return;
                 }
 
-                LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value)
+                LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value ?? new StarDifficulty())
                 {
                     Shear = -Shear,
                     Depth = Info?.Depth + 1 ?? 0
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index e66469ff8d..633ef9297e 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Screens.Select.Carousel
         [Resolved(CanBeNull = true)]
         private ManageCollectionsDialog manageCollectionsDialog { get; set; }
 
-        private IBindable<StarDifficulty> starDifficultyBindable;
+        private IBindable<StarDifficulty?> starDifficultyBindable;
         private CancellationTokenSource starDifficultyCancellationSource;
 
         public DrawableCarouselBeatmap(CarouselBeatmap panel)
@@ -217,7 +217,10 @@ namespace osu.Game.Screens.Select.Carousel
             {
                 // We've potentially cancelled the computation above so a new bindable is required.
                 starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);
-                starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true);
+                starDifficultyBindable.BindValueChanged(d =>
+                {
+                    starCounter.Current = (float)(d.NewValue?.Stars ?? 0);
+                }, true);
             }
 
             base.ApplyState();

From 5fa9bf61b6a8d2abfd374759da0553d8e807bc27 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 16:22:40 +0900
Subject: [PATCH 076/434] Update xmldoc

---
 osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
index 72a9b36c6f..53d82c385d 100644
--- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps
         /// </summary>
         /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
         /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
-        /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
+        /// <returns>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).</returns>
         public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
         {
             var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
@@ -90,7 +90,7 @@ namespace osu.Game.Beatmaps
         /// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
         /// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
         /// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
-        /// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
+        /// <returns>A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state.</returns>
         public IBindable<StarDifficulty?> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
                                                                 CancellationToken cancellationToken = default)
             => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);

From 31c52bd585a55f925676313671c23c437aff28e2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 17:00:42 +0900
Subject: [PATCH 077/434] Update the displayed BPM at song select with rate
 adjust mods

This only covers constant rate rate adjust mods. Mods like wind up/wind
down will need a more complex implementation which we haven't really
planned yet.
---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 86cb561bc7..13ec106694 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -383,10 +383,18 @@ namespace osu.Game.Screens.Select
                 return labels.ToArray();
             }
 
+            [Resolved]
+            private IBindable<IReadOnlyList<Mod>> mods { get; set; }
+
             private string getBPMRange(IBeatmap beatmap)
             {
-                double bpmMax = beatmap.ControlPointInfo.BPMMaximum;
-                double bpmMin = beatmap.ControlPointInfo.BPMMinimum;
+                // this doesn't consider mods which apply variable rates, yet.
+                double rate = 1;
+                foreach (var mod in mods.Value.OfType<IApplicableToRate>())
+                    rate = mod.ApplyToRate(0, rate);
+
+                double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;
+                double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate;
 
                 if (Precision.AlmostEquals(bpmMin, bpmMax))
                     return $"{bpmMin:0}";

From 2db4b793d7ab2c064c1a6a1b924ef379b71a86ee Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 17:04:39 +0900
Subject: [PATCH 078/434] Also handle most common BPM display

---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 13ec106694..311ed6ffb9 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -395,11 +395,12 @@ namespace osu.Game.Screens.Select
 
                 double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;
                 double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate;
+                double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate;
 
                 if (Precision.AlmostEquals(bpmMin, bpmMax))
                     return $"{bpmMin:0}";
 
-                return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})";
+                return $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
             }
 
             private OsuSpriteText[] getMapper(BeatmapMetadata metadata)

From 6d1c5979eafaf2dac1182b49c79c0e3a9e369c4d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 17:28:59 +0900
Subject: [PATCH 079/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 183ac61c90..8ea7cfac5b 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.222.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.225.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 37d730bf42..6ff08ae63c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.225.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.1" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index ca11952cc8..d7a1b7d692 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.222.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.225.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -91,7 +91,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.222.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.225.0" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 3802cb29a42056927da2f2c5535cd55bbfc5cf0f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Feb 2021 17:46:35 +0900
Subject: [PATCH 080/434] Fix failing tests doing reference comparisons between
 string and LocalisedString

---
 .../Ranking/TestSceneExpandedPanelMiddleContent.cs |  4 ++--
 .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 14 +++++++-------
 .../UserInterface/TestSceneFooterButtonMods.cs     |  2 +-
 osu.Game/Configuration/SettingSourceAttribute.cs   |  3 ++-
 4 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index f9fe42131f..2f558a6379 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Ranking
                 Beatmap = createTestBeatmap(author)
             }));
 
-            AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Text == "mapper_name"));
+            AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Current.Value == "mapper_name"));
         }
 
         [Test]
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
             }));
 
             AddAssert("mapped by text not present", () =>
-                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
+                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Current.Value, "mapped", "by")));
         }
 
         private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index fff4a9ba61..07b67ca3ad 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -103,10 +103,10 @@ namespace osu.Game.Tests.Visual.SongSelect
 
         private void testBeatmapLabels(Ruleset ruleset)
         {
-            AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version");
-            AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
-            AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Text == $"{ruleset.ShortName}Artist");
-            AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType<OsuSpriteText>().Any(s => s.Text == $"{ruleset.ShortName}Author"));
+            AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version");
+            AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
+            AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
+            AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author"));
         }
 
         private void testInfoLabels(int expectedCount)
@@ -119,9 +119,9 @@ namespace osu.Game.Tests.Visual.SongSelect
         public void TestNullBeatmap()
         {
             selectBeatmap(null);
-            AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text.ToString()));
-            AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title);
-            AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist);
+            AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value));
+            AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
+            AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
             AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
             AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
         }
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
index 1e3b1c2ffd..546e905ded 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.UserInterface
             var multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
             var expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x";
 
-            return expectedValue == footerButtonMods.MultiplierText.Text;
+            return expectedValue == footerButtonMods.MultiplierText.Current.Value;
         }
 
         private class TestFooterButtonMods : FooterButtonMods
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 70d67aaaa0..65a5a6d1b4 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -8,6 +8,7 @@ using System.Reflection;
 using JetBrains.Annotations;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
+using osu.Framework.Localisation;
 using osu.Game.Overlays.Settings;
 
 namespace osu.Game.Configuration
@@ -24,7 +25,7 @@ namespace osu.Game.Configuration
     [AttributeUsage(AttributeTargets.Property)]
     public class SettingSourceAttribute : Attribute
     {
-        public string Label { get; }
+        public LocalisableString Label { get; }
 
         public string Description { get; }
 

From cf4c88c647f2bbfc03984218b01d8dc81d396bbe Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 21:38:21 +0900
Subject: [PATCH 081/434] Fix spacing

---
 .../Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 8e9020ee13..54c37e9742 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -115,10 +115,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                 }
 
                 if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
-                                                           // If we convert to 7K + 1, let's not overload the special key
-                                                           && (TotalColumns != 8 || lastColumn != 0)
-                                                           // Make sure the last column was not the centre column
-                                                           && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
+                                                               // If we convert to 7K + 1, let's not overload the special key
+                                                               && (TotalColumns != 8 || lastColumn != 0)
+                                                               // Make sure the last column was not the centre column
+                                                               && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
                 {
                     // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
                     int column = RandomStart + TotalColumns - lastColumn - 1;

From 98313a98bf4e0b89deb9b47219fc4e0fd3aaef4a Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 21:48:02 +0900
Subject: [PATCH 082/434] DI mods in parent class and pass them down

---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 311ed6ffb9..37808f6e94 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Screens.Select
         private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
 
         private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
+        private readonly IBindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
 
         [Resolved]
         private BeatmapDifficultyCache difficultyCache { get; set; }
@@ -64,9 +65,11 @@ namespace osu.Game.Screens.Select
         }
 
         [BackgroundDependencyLoader(true)]
-        private void load([CanBeNull] Bindable<RulesetInfo> parentRuleset)
+        private void load([CanBeNull] Bindable<RulesetInfo> parentRuleset, [CanBeNull] Bindable<IReadOnlyList<Mod>> parentMods)
         {
             ruleset.BindTo(parentRuleset);
+            mods.BindTo(parentMods);
+
             ruleset.ValueChanged += _ => updateDisplay();
         }
 
@@ -132,7 +135,7 @@ namespace osu.Game.Screens.Select
                     return;
                 }
 
-                LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value)
+                LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value)
                 {
                     Shear = -Shear,
                     Depth = Info?.Depth + 1 ?? 0
@@ -167,13 +170,15 @@ namespace osu.Game.Screens.Select
 
             private readonly WorkingBeatmap beatmap;
             private readonly RulesetInfo ruleset;
+            private readonly IReadOnlyList<Mod> mods;
             private readonly StarDifficulty starDifficulty;
 
-            public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, StarDifficulty difficulty)
+            public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
                 : base(pixelSnapping: true)
             {
                 this.beatmap = beatmap;
                 ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
+                this.mods = mods;
                 starDifficulty = difficulty;
             }
 
@@ -383,14 +388,11 @@ namespace osu.Game.Screens.Select
                 return labels.ToArray();
             }
 
-            [Resolved]
-            private IBindable<IReadOnlyList<Mod>> mods { get; set; }
-
             private string getBPMRange(IBeatmap beatmap)
             {
                 // this doesn't consider mods which apply variable rates, yet.
                 double rate = 1;
-                foreach (var mod in mods.Value.OfType<IApplicableToRate>())
+                foreach (var mod in mods.OfType<IApplicableToRate>())
                     rate = mod.ApplyToRate(0, rate);
 
                 double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;

From de417a660d7121589abb9c0b0fe635f4e2f44eb0 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 21:51:32 +0900
Subject: [PATCH 083/434] Make BPM update with changes in mod settings

---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 113 ++++++++++++--------
 1 file changed, 68 insertions(+), 45 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 37808f6e94..9084435f44 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -25,6 +25,7 @@ using osu.Framework.Graphics.Effects;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Localisation;
 using osu.Framework.Logging;
+using osu.Game.Configuration;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.UI;
@@ -167,12 +168,15 @@ namespace osu.Game.Screens.Select
 
             private ILocalisedBindableString titleBinding;
             private ILocalisedBindableString artistBinding;
+            private Container bpmLabelContainer;
 
             private readonly WorkingBeatmap beatmap;
             private readonly RulesetInfo ruleset;
             private readonly IReadOnlyList<Mod> mods;
             private readonly StarDifficulty starDifficulty;
 
+            private ModSettingChangeTracker settingChangeTracker;
+
             public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
                 : base(pixelSnapping: true)
             {
@@ -189,9 +193,11 @@ namespace osu.Game.Screens.Select
                 var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
 
                 CacheDrawnFrameBuffer = true;
-
                 RelativeSizeAxes = Axes.Both;
 
+                settingChangeTracker = new ModSettingChangeTracker(mods);
+                settingChangeTracker.SettingChanged += _ => updateBPM();
+
                 titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title)));
                 artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist)));
 
@@ -312,7 +318,25 @@ namespace osu.Game.Screens.Select
                                 Margin = new MarginPadding { Top = 20 },
                                 Spacing = new Vector2(20, 0),
                                 AutoSizeAxes = Axes.Both,
-                                Children = getInfoLabels()
+                                Children = new Drawable[]
+                                {
+                                    new InfoLabel(new BeatmapStatistic
+                                    {
+                                        Name = "Length",
+                                        CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
+                                        Content = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToString(@"m\:ss"),
+                                    }),
+                                    bpmLabelContainer = new Container
+                                    {
+                                        AutoSizeAxes = Axes.Both,
+                                    },
+                                    new FillFlowContainer
+                                    {
+                                        AutoSizeAxes = Axes.Both,
+                                        Spacing = new Vector2(20, 0),
+                                        Children = getRulesetInfoLabels()
+                                    }
+                                }
                             }
                         }
                     }
@@ -324,6 +348,8 @@ namespace osu.Game.Screens.Select
                 // no difficulty means it can't have a status to show
                 if (beatmapInfo.Version == null)
                     StatusPill.Hide();
+
+                updateBPM();
             }
 
             private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0
@@ -340,69 +366,60 @@ namespace osu.Game.Screens.Select
                 ForceRedraw();
             }
 
-            private InfoLabel[] getInfoLabels()
+            private InfoLabel[] getRulesetInfoLabels()
             {
-                var b = beatmap.Beatmap;
-
-                List<InfoLabel> labels = new List<InfoLabel>();
-
-                if (b?.HitObjects?.Any() == true)
+                try
                 {
-                    labels.Add(new InfoLabel(new BeatmapStatistic
-                    {
-                        Name = "Length",
-                        CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
-                        Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"),
-                    }));
-
-                    labels.Add(new InfoLabel(new BeatmapStatistic
-                    {
-                        Name = "BPM",
-                        CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
-                        Content = getBPMRange(b),
-                    }));
+                    IBeatmap playableBeatmap;
 
                     try
                     {
-                        IBeatmap playableBeatmap;
-
-                        try
-                        {
-                            // Try to get the beatmap with the user's ruleset
-                            playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
-                        }
-                        catch (BeatmapInvalidForRulesetException)
-                        {
-                            // Can't be converted to the user's ruleset, so use the beatmap's own ruleset
-                            playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
-                        }
-
-                        labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)));
+                        // Try to get the beatmap with the user's ruleset
+                        playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty<Mod>());
                     }
-                    catch (Exception e)
+                    catch (BeatmapInvalidForRulesetException)
                     {
-                        Logger.Error(e, "Could not load beatmap successfully!");
+                        // Can't be converted to the user's ruleset, so use the beatmap's own ruleset
+                        playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty<Mod>());
                     }
+
+                    return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray();
+                }
+                catch (Exception e)
+                {
+                    Logger.Error(e, "Could not load beatmap successfully!");
                 }
 
-                return labels.ToArray();
+                return Array.Empty<InfoLabel>();
             }
 
-            private string getBPMRange(IBeatmap beatmap)
+            private void updateBPM()
             {
+                var b = beatmap.Beatmap;
+                if (b == null)
+                    return;
+
                 // this doesn't consider mods which apply variable rates, yet.
                 double rate = 1;
                 foreach (var mod in mods.OfType<IApplicableToRate>())
                     rate = mod.ApplyToRate(0, rate);
 
-                double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate;
-                double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate;
-                double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate;
+                double bpmMax = b.ControlPointInfo.BPMMaximum * rate;
+                double bpmMin = b.ControlPointInfo.BPMMinimum * rate;
+                double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate;
 
-                if (Precision.AlmostEquals(bpmMin, bpmMax))
-                    return $"{bpmMin:0}";
+                string labelText = Precision.AlmostEquals(bpmMin, bpmMax)
+                    ? $"{bpmMin:0}"
+                    : $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
 
-                return $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})";
+                bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic
+                {
+                    Name = "BPM",
+                    CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
+                    Content = labelText
+                });
+
+                ForceRedraw();
             }
 
             private OsuSpriteText[] getMapper(BeatmapMetadata metadata)
@@ -425,6 +442,12 @@ namespace osu.Game.Screens.Select
                 };
             }
 
+            protected override void Dispose(bool isDisposing)
+            {
+                base.Dispose(isDisposing);
+                settingChangeTracker?.Dispose();
+            }
+
             public class InfoLabel : Container, IHasTooltip
             {
                 public string TooltipText { get; }

From 649ce20e354b2f30b08a3c60db94695e24aa5e34 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 22:01:53 +0900
Subject: [PATCH 084/434] Fix up super weird and super wrong DI

---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index d1b28e6607..97fe099975 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
-using JetBrains.Annotations;
 using osuTK;
 using osuTK.Graphics;
 using osu.Framework.Allocation;
@@ -39,8 +38,11 @@ namespace osu.Game.Screens.Select
 
         private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
 
-        private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
-        private readonly IBindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
+        [Resolved]
+        private IBindable<RulesetInfo> ruleset { get; set; }
+
+        [Resolved]
+        private IBindable<IReadOnlyList<Mod>> mods { get; set; }
 
         [Resolved]
         private BeatmapDifficultyCache difficultyCache { get; set; }
@@ -65,13 +67,10 @@ namespace osu.Game.Screens.Select
             };
         }
 
-        [BackgroundDependencyLoader(true)]
-        private void load([CanBeNull] Bindable<RulesetInfo> parentRuleset, [CanBeNull] Bindable<IReadOnlyList<Mod>> parentMods)
+        protected override void LoadComplete()
         {
-            ruleset.BindTo(parentRuleset);
-            mods.BindTo(parentMods);
-
-            ruleset.ValueChanged += _ => updateDisplay();
+            base.LoadComplete();
+            ruleset.BindValueChanged(_ => updateDisplay(), true);
         }
 
         protected override void PopIn()

From c3eb44137bfd109006f826fc196a2394a2675196 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 22:09:41 +0900
Subject: [PATCH 085/434] Move ValueChanged bind back to load()

---
 osu.Game/Screens/Select/BeatmapInfoWedge.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 97fe099975..fe2b7b7525 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -67,10 +67,10 @@ namespace osu.Game.Screens.Select
             };
         }
 
-        protected override void LoadComplete()
+        [BackgroundDependencyLoader]
+        private void load()
         {
-            base.LoadComplete();
-            ruleset.BindValueChanged(_ => updateDisplay(), true);
+            ruleset.BindValueChanged(_ => updateDisplay());
         }
 
         protected override void PopIn()

From 01a48154126fbc2ca75cea6632349689ea41ce4f Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 25 Feb 2021 23:36:02 +0900
Subject: [PATCH 086/434] Make labels disappear on null beatmap/no hitobjects

---
 .../SongSelect/TestSceneBeatmapInfoWedge.cs   |  7 ++-
 osu.Game/Screens/Select/BeatmapInfoWedge.cs   | 61 +++++++++++--------
 2 files changed, 39 insertions(+), 29 deletions(-)

diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index 07b67ca3ad..7ea6373763 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
@@ -7,6 +7,7 @@ using JetBrains.Annotations;
 using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
+using osu.Framework.Testing;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets;
@@ -111,8 +112,8 @@ namespace osu.Game.Tests.Visual.SongSelect
 
         private void testInfoLabels(int expectedCount)
         {
-            AddAssert("check info labels exists", () => infoWedge.Info.InfoLabelContainer.Children.Any());
-            AddAssert("check info labels count", () => infoWedge.Info.InfoLabelContainer.Children.Count == expectedCount);
+            AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
+            AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount);
         }
 
         [Test]
@@ -123,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
             AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
             AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
-            AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
+            AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
         }
 
         [Test]
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index fe2b7b7525..36cc19cce3 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -163,10 +163,10 @@ namespace osu.Game.Screens.Select
             public OsuSpriteText ArtistLabel { get; private set; }
             public BeatmapSetOnlineStatusPill StatusPill { get; private set; }
             public FillFlowContainer MapperContainer { get; private set; }
-            public FillFlowContainer InfoLabelContainer { get; private set; }
 
             private ILocalisedBindableString titleBinding;
             private ILocalisedBindableString artistBinding;
+            private FillFlowContainer infoLabelContainer;
             private Container bpmLabelContainer;
 
             private readonly WorkingBeatmap beatmap;
@@ -194,9 +194,6 @@ namespace osu.Game.Screens.Select
                 CacheDrawnFrameBuffer = true;
                 RelativeSizeAxes = Axes.Both;
 
-                settingChangeTracker = new ModSettingChangeTracker(mods);
-                settingChangeTracker.SettingChanged += _ => updateBPM();
-
                 titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
                 artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist));
 
@@ -312,30 +309,11 @@ namespace osu.Game.Screens.Select
                                 AutoSizeAxes = Axes.Both,
                                 Children = getMapper(metadata)
                             },
-                            InfoLabelContainer = new FillFlowContainer
+                            infoLabelContainer = new FillFlowContainer
                             {
                                 Margin = new MarginPadding { Top = 20 },
                                 Spacing = new Vector2(20, 0),
                                 AutoSizeAxes = Axes.Both,
-                                Children = new Drawable[]
-                                {
-                                    new InfoLabel(new BeatmapStatistic
-                                    {
-                                        Name = "Length",
-                                        CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
-                                        Content = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToString(@"m\:ss"),
-                                    }),
-                                    bpmLabelContainer = new Container
-                                    {
-                                        AutoSizeAxes = Axes.Both,
-                                    },
-                                    new FillFlowContainer
-                                    {
-                                        AutoSizeAxes = Axes.Both,
-                                        Spacing = new Vector2(20, 0),
-                                        Children = getRulesetInfoLabels()
-                                    }
-                                }
                             }
                         }
                     }
@@ -348,7 +326,7 @@ namespace osu.Game.Screens.Select
                 if (beatmapInfo.Version == null)
                     StatusPill.Hide();
 
-                updateBPM();
+                addInfoLabels();
             }
 
             private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0
@@ -365,6 +343,37 @@ namespace osu.Game.Screens.Select
                 ForceRedraw();
             }
 
+            private void addInfoLabels()
+            {
+                if (beatmap.Beatmap?.HitObjects?.Any() != true)
+                    return;
+
+                infoLabelContainer.Children = new Drawable[]
+                {
+                    new InfoLabel(new BeatmapStatistic
+                    {
+                        Name = "Length",
+                        CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length),
+                        Content = TimeSpan.FromMilliseconds(beatmap.BeatmapInfo.Length).ToString(@"m\:ss"),
+                    }),
+                    bpmLabelContainer = new Container
+                    {
+                        AutoSizeAxes = Axes.Both,
+                    },
+                    new FillFlowContainer
+                    {
+                        AutoSizeAxes = Axes.Both,
+                        Spacing = new Vector2(20, 0),
+                        Children = getRulesetInfoLabels()
+                    }
+                };
+
+                settingChangeTracker = new ModSettingChangeTracker(mods);
+                settingChangeTracker.SettingChanged += _ => refreshBPMLabel();
+
+                refreshBPMLabel();
+            }
+
             private InfoLabel[] getRulesetInfoLabels()
             {
                 try
@@ -392,7 +401,7 @@ namespace osu.Game.Screens.Select
                 return Array.Empty<InfoLabel>();
             }
 
-            private void updateBPM()
+            private void refreshBPMLabel()
             {
                 var b = beatmap.Beatmap;
                 if (b == null)

From 254f9bb58be27c0981dd32df8a1d6038a6a090fb Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 13:37:58 +0900
Subject: [PATCH 087/434] Show API human readable error message when chat
 posting fails

Closes #11902.
---
 osu.Game/Online/Chat/ChannelManager.cs | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 036ec4d0f3..a980f4c54b 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Online.Chat
 
                     createNewPrivateMessageRequest.Failure += exception =>
                     {
-                        Logger.Error(exception, "Posting message failed.");
+                        handlePostException(exception);
                         target.ReplaceMessage(message, null);
                         dequeueAndRun();
                     };
@@ -171,7 +171,7 @@ namespace osu.Game.Online.Chat
 
                 req.Failure += exception =>
                 {
-                    Logger.Error(exception, "Posting message failed.");
+                    handlePostException(exception);
                     target.ReplaceMessage(message, null);
                     dequeueAndRun();
                 };
@@ -184,6 +184,14 @@ namespace osu.Game.Online.Chat
                 dequeueAndRun();
         }
 
+        private static void handlePostException(Exception exception)
+        {
+            if (exception is APIException apiException)
+                Logger.Log(apiException.Message, level: LogLevel.Important);
+            else
+                Logger.Error(exception, "Posting message failed.");
+        }
+
         /// <summary>
         /// Posts a command locally. Commands like /help will result in a help message written in the current channel.
         /// </summary>

From cd1c1bf534947585db53eb6b4641acef41f1a12f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 14:15:12 +0900
Subject: [PATCH 088/434] Centralise cases of performing actions on the current
 selection

By moving this to a central location, we can avoid invoking the
EditorChangeHandler when there is no selection made. This helps
alleviate the issue pointed out in
https://github.com/ppy/osu/issues/11901, but not fix it completely.
---
 .../Edit/ManiaSelectionHandler.cs             |  8 +++--
 .../Edit/TaikoSelectionHandler.cs             | 32 +++++++------------
 .../Compose/Components/BlueprintContainer.cs  |  3 +-
 .../Compose/Components/SelectionHandler.cs    | 27 ++++------------
 osu.Game/Screens/Edit/EditorBeatmap.cs        | 16 ++++++++++
 5 files changed, 42 insertions(+), 44 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 50629f41a9..2689ed4112 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
             int minColumn = int.MaxValue;
             int maxColumn = int.MinValue;
 
+            // find min/max in an initial pass before actually performing the movement.
             foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
             {
                 if (obj.Column < minColumn)
@@ -55,8 +56,11 @@ namespace osu.Game.Rulesets.Mania.Edit
 
             columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
 
-            foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
-                obj.Column += columnDelta;
+            EditorBeatmap.PerformOnSelection(h =>
+            {
+                if (h is ManiaHitObject maniaObj)
+                    maniaObj.Column += columnDelta;
+            });
         }
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index 3fbcee44af..ac2dd4bdb6 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -52,32 +52,24 @@ namespace osu.Game.Rulesets.Taiko.Edit
 
         public void SetStrongState(bool state)
         {
-            var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
-
-            EditorBeatmap.BeginChange();
-
-            foreach (var h in hits)
+            EditorBeatmap.PerformOnSelection(h =>
             {
-                if (h.IsStrong != state)
-                {
-                    h.IsStrong = state;
-                    EditorBeatmap.Update(h);
-                }
-            }
+                if (!(h is Hit taikoHit)) return;
 
-            EditorBeatmap.EndChange();
+                if (taikoHit.IsStrong != state)
+                {
+                    taikoHit.IsStrong = state;
+                    EditorBeatmap.Update(taikoHit);
+                }
+            });
         }
 
         public void SetRimState(bool state)
         {
-            var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
-
-            EditorBeatmap.BeginChange();
-
-            foreach (var h in hits)
-                h.Type = state ? HitType.Rim : HitType.Centre;
-
-            EditorBeatmap.EndChange();
+            EditorBeatmap.PerformOnSelection(h =>
+            {
+                if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre;
+            });
         }
 
         protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 5371beac60..051d0766bf 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -495,8 +495,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
                 // Apply the start time at the newly snapped-to position
                 double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
 
-                foreach (HitObject obj in Beatmap.SelectedHitObjects)
-                    obj.StartTime += offset;
+                Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
             }
 
             return true;
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 788b485449..018d4d081c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -320,18 +320,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
         /// <param name="sampleName">The name of the hit sample.</param>
         public void AddHitSample(string sampleName)
         {
-            EditorBeatmap.BeginChange();
-
-            foreach (var h in EditorBeatmap.SelectedHitObjects)
+            EditorBeatmap.PerformOnSelection(h =>
             {
                 // Make sure there isn't already an existing sample
                 if (h.Samples.Any(s => s.Name == sampleName))
-                    continue;
+                    return;
 
                 h.Samples.Add(new HitSampleInfo(sampleName));
-            }
-
-            EditorBeatmap.EndChange();
+            });
         }
 
         /// <summary>
@@ -341,19 +337,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
         /// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
         public void SetNewCombo(bool state)
         {
-            EditorBeatmap.BeginChange();
-
-            foreach (var h in EditorBeatmap.SelectedHitObjects)
+            EditorBeatmap.PerformOnSelection(h =>
             {
                 var comboInfo = h as IHasComboInformation;
 
-                if (comboInfo == null || comboInfo.NewCombo == state) continue;
+                if (comboInfo == null || comboInfo.NewCombo == state) return;
 
                 comboInfo.NewCombo = state;
                 EditorBeatmap.Update(h);
-            }
-
-            EditorBeatmap.EndChange();
+            });
         }
 
         /// <summary>
@@ -362,12 +354,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
         /// <param name="sampleName">The name of the hit sample.</param>
         public void RemoveHitSample(string sampleName)
         {
-            EditorBeatmap.BeginChange();
-
-            foreach (var h in EditorBeatmap.SelectedHitObjects)
-                h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
-
-            EditorBeatmap.EndChange();
+            EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
         }
 
         #endregion
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 174ff1478b..4f1b0484d2 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -100,6 +100,22 @@ namespace osu.Game.Screens.Edit
 
         private readonly HashSet<HitObject> batchPendingUpdates = new HashSet<HitObject>();
 
+        /// <summary>
+        /// Perform the provided action on every selected hitobject.
+        /// Changes will be grouped as one history action.
+        /// </summary>
+        /// <param name="action">The action to perform.</param>
+        public void PerformOnSelection(Action<HitObject> action)
+        {
+            if (SelectedHitObjects.Count == 0)
+                return;
+
+            BeginChange();
+            foreach (var h in SelectedHitObjects)
+                action(h);
+            EndChange();
+        }
+
         /// <summary>
         /// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
         /// </summary>

From 3e65dfb9e7df4fe11c7d884e85efb30e461041b6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 17:11:47 +0900
Subject: [PATCH 089/434] Reduce allocation overhead when notification overlay
 has visible notifications

---
 .../Notifications/NotificationSection.cs        | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs
index bc41311a6d..2316199049 100644
--- a/osu.Game/Overlays/Notifications/NotificationSection.cs
+++ b/osu.Game/Overlays/Notifications/NotificationSection.cs
@@ -10,9 +10,9 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Localisation;
 using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osuTK;
-using osu.Game.Graphics.Containers;
 
 namespace osu.Game.Overlays.Notifications
 {
@@ -122,7 +122,20 @@ namespace osu.Game.Overlays.Notifications
         {
             base.Update();
 
-            countDrawable.Text = notifications.Children.Count(c => c.Alpha > 0.99f).ToString();
+            countDrawable.Text = getVisibleCount().ToString();
+        }
+
+        private int getVisibleCount()
+        {
+            int count = 0;
+
+            foreach (var c in notifications)
+            {
+                if (c.Alpha > 0.99f)
+                    count++;
+            }
+
+            return count;
         }
 
         private class ClearAllButton : OsuClickableContainer

From 7e6bd0e995fe8ec1f33b5cbfa510ad7cac69c04e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 17:30:59 +0900
Subject: [PATCH 090/434] Fix "failed to import" message showing when importing
 from a stable install with no beatmaps

---
 osu.Game/Database/ArchiveModelManager.cs | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 03b8db2cb8..daaba9098e 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -141,6 +141,13 @@ namespace osu.Game.Database
 
         protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params ImportTask[] tasks)
         {
+            if (tasks.Length == 0)
+            {
+                notification.CompletionText = $"No {HumanisedModelName}s were found to import!";
+                notification.State = ProgressNotificationState.Completed;
+                return Enumerable.Empty<TModel>();
+            }
+
             notification.Progress = 0;
             notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
 

From 1ab449b73e081284f88125c696845c51c35ae984 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 17:54:51 +0900
Subject: [PATCH 091/434] Add test scene for drawings screen

---
 .../Screens/TestSceneDrawingsScreen.cs        | 35 +++++++++++++++++++
 1 file changed, 35 insertions(+)
 create mode 100644 osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs

diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs
new file mode 100644
index 0000000000..e2954c8f10
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.IO;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game.Graphics.Cursor;
+using osu.Game.Tournament.Screens.Drawings;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+    public class TestSceneDrawingsScreen : TournamentTestScene
+    {
+        [BackgroundDependencyLoader]
+        private void load(Storage storage)
+        {
+            using (var stream = storage.GetStream("drawings.txt", FileAccess.Write))
+            using (var writer = new StreamWriter(stream))
+            {
+                writer.WriteLine("KR : South Korea : KOR");
+                writer.WriteLine("US : United States : USA");
+                writer.WriteLine("PH : Philippines : PHL");
+                writer.WriteLine("BR : Brazil : BRA");
+                writer.WriteLine("JP : Japan : JPN");
+            }
+
+            Add(new OsuContextMenuContainer
+            {
+                RelativeSizeAxes = Axes.Both,
+                Child = new DrawingsScreen()
+            });
+        }
+    }
+}

From 1ac82af19abce0e1e2ce97facf22808652d9d305 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 17:58:21 +0900
Subject: [PATCH 092/434] Adjust flag size to fit again

---
 .../Screens/Drawings/Components/ScrollingTeamContainer.cs       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
index 3ff4718b75..c7060bd538 100644
--- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
@@ -345,7 +345,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
 
                 Flag.Anchor = Anchor.Centre;
                 Flag.Origin = Anchor.Centre;
-                Flag.Scale = new Vector2(0.9f);
+                Flag.Scale = new Vector2(0.7f);
 
                 InternalChildren = new Drawable[]
                 {

From 98d525d1dbb6f8b854b682a45d5ba600f30da6ea Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 19:56:10 +0900
Subject: [PATCH 093/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 8ea7cfac5b..5d83bb9583 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.225.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.226.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 6ff08ae63c..84a74502c2 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.225.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.1" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index d7a1b7d692..2cea2e4b13 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.225.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.226.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -91,7 +91,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.225.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
     <PackageReference Include="SharpCompress" Version="0.27.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 4fd8501c860989e09f1fdfedc9405bdde39aa70c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Feb 2021 20:03:03 +0900
Subject: [PATCH 094/434] Remove unnecessary using (underlying enumerator
 change)

---
 osu.Game/Screens/Play/PlayerLoader.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 5b4bd11216..7d906cdc5b 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -3,7 +3,6 @@
 
 using System;
 using System.Diagnostics;
-using System.Linq;
 using System.Threading.Tasks;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;

From 52e81385a6ba594cd325e24cf71c6580a7922727 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 11:33:08 +0100
Subject: [PATCH 095/434] Fix restore default button mutating transforms during
 load

---
 osu.Game/Overlays/Settings/SettingsItem.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index aafd7463a6..4cb8d7f83c 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -207,7 +207,9 @@ namespace osu.Game.Overlays.Settings
                 UpdateState();
             }
 
-            public void UpdateState()
+            public void UpdateState() => Scheduler.AddOnce(updateState);
+
+            private void updateState()
             {
                 if (bindable == null)
                     return;

From 87b73da73edddc47f93d5c1a5edc6f8bae3ce6fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 14:46:48 +0100
Subject: [PATCH 096/434] Add failing test case

---
 .../Mods/SettingsSourceAttributeTest.cs       | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs

diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
new file mode 100644
index 0000000000..240d617dc7
--- /dev/null
+++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
+
+namespace osu.Game.Tests.Mods
+{
+    [TestFixture]
+    public class SettingsSourceAttributeTest
+    {
+        [Test]
+        public void TestOrdering()
+        {
+            var objectWithSettings = new ClassWithSettings();
+
+            var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray();
+
+            Assert.That(orderedSettings, Has.Length.EqualTo(3));
+        }
+
+        private class ClassWithSettings
+        {
+            [SettingSource("Second setting", "Another description", 2)]
+            public BindableBool SecondSetting { get; set; } = new BindableBool();
+
+            [SettingSource("First setting", "A description", 1)]
+            public BindableDouble FirstSetting { get; set; } = new BindableDouble();
+
+            [SettingSource("Third setting", "Yet another description", 3)]
+            public BindableInt ThirdSetting { get; set; } = new BindableInt();
+        }
+    }
+}

From 528de5869e305ec377d75098496ecaeadb949b69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 14:47:09 +0100
Subject: [PATCH 097/434] Fix multiple enumerations when ordering setting
 sources

This was not spotted previously, because the base `Attribute` overrides
`Equals()` to have semantics similar to structs (per-field equality) by
using reflection. That masked the issue when strings were used, and
migrating to `LocalisableString` revealed it, as that struct's
implementation of equality currently uses instance checks.

Whether `LocalisableString.Equals()` is the correct implementation may
still be up for discussion, but allowing multiple enumeration is wrong
anyway, since the underlying enumerables are live (one especially is a
yield iterator, causing new object instances to be allocated).
---
 osu.Game/Configuration/SettingSourceAttribute.cs | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 65a5a6d1b4..d0d2480e62 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -139,9 +139,12 @@ namespace osu.Game.Configuration
 
         public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj)
         {
-            var original = obj.GetSettingsSourceProperties();
+            var original = obj.GetSettingsSourceProperties().ToArray();
 
-            var orderedRelative = original.Where(attr => attr.Item1.OrderPosition != null).OrderBy(attr => attr.Item1.OrderPosition);
+            var orderedRelative = original
+                                  .Where(attr => attr.Item1.OrderPosition != null)
+                                  .OrderBy(attr => attr.Item1.OrderPosition)
+                                  .ToArray();
             var unordered = original.Except(orderedRelative);
 
             return orderedRelative.Concat(unordered);

From dd2f63f3137c8a9923509f04b2bb5656eda0093a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 14:57:37 +0100
Subject: [PATCH 098/434] Add assertions to actually check order

---
 osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
index 240d617dc7..7fce1a6ce5 100644
--- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
+++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
@@ -19,6 +19,10 @@ namespace osu.Game.Tests.Mods
             var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray();
 
             Assert.That(orderedSettings, Has.Length.EqualTo(3));
+
+            Assert.That(orderedSettings[0].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.FirstSetting)));
+            Assert.That(orderedSettings[1].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.SecondSetting)));
+            Assert.That(orderedSettings[2].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.ThirdSetting)));
         }
 
         private class ClassWithSettings

From 7b6e53680c6035f6b7843f3ac5bd65b90e41128d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 15:14:25 +0100
Subject: [PATCH 099/434] Add coverage for the unordered case

---
 osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
index 7fce1a6ce5..883c9d1ac2 100644
--- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
+++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
@@ -18,15 +18,19 @@ namespace osu.Game.Tests.Mods
 
             var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray();
 
-            Assert.That(orderedSettings, Has.Length.EqualTo(3));
+            Assert.That(orderedSettings, Has.Length.EqualTo(4));
 
             Assert.That(orderedSettings[0].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.FirstSetting)));
             Assert.That(orderedSettings[1].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.SecondSetting)));
             Assert.That(orderedSettings[2].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.ThirdSetting)));
+            Assert.That(orderedSettings[3].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.UnorderedSetting)));
         }
 
         private class ClassWithSettings
         {
+            [SettingSource("Unordered setting", "Should be last")]
+            public BindableFloat UnorderedSetting { get; set; } = new BindableFloat();
+
             [SettingSource("Second setting", "Another description", 2)]
             public BindableBool SecondSetting { get; set; } = new BindableBool();
 

From 1e56d2cbba16edec86632b5c1f780f1abaac2d1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 15:30:05 +0100
Subject: [PATCH 100/434] Make `SettingSourceAttribute` implement `IComparable`

---
 .../Configuration/SettingSourceAttribute.cs   | 29 +++++++++++++------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index d0d2480e62..4cc31e14ac 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Configuration
     /// </remarks>
     [MeansImplicitUse]
     [AttributeUsage(AttributeTargets.Property)]
-    public class SettingSourceAttribute : Attribute
+    public class SettingSourceAttribute : Attribute, IComparable<SettingSourceAttribute>
     {
         public LocalisableString Label { get; }
 
@@ -42,6 +42,21 @@ namespace osu.Game.Configuration
         {
             OrderPosition = orderPosition;
         }
+
+        public int CompareTo(SettingSourceAttribute other)
+        {
+            if (OrderPosition == other.OrderPosition)
+                return 0;
+
+            // unordered items come last (are greater than any ordered items).
+            if (OrderPosition == null)
+                return 1;
+            if (other.OrderPosition == null)
+                return -1;
+
+            // ordered items are sorted by the order value.
+            return OrderPosition.Value.CompareTo(other.OrderPosition);
+        }
     }
 
     public static class SettingSourceExtensions
@@ -137,17 +152,13 @@ namespace osu.Game.Configuration
             }
         }
 
-        public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj)
+        public static ICollection<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj)
         {
             var original = obj.GetSettingsSourceProperties().ToArray();
 
-            var orderedRelative = original
-                                  .Where(attr => attr.Item1.OrderPosition != null)
-                                  .OrderBy(attr => attr.Item1.OrderPosition)
-                                  .ToArray();
-            var unordered = original.Except(orderedRelative);
-
-            return orderedRelative.Concat(unordered);
+            return original
+                   .OrderBy(attr => attr.Item1)
+                   .ToArray();
         }
     }
 }

From 7e17c5ab7180c460f6fe142a37a2b0fdf3b8c987 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 27 Feb 2021 15:46:18 +0100
Subject: [PATCH 101/434] Trim yet another array copy

---
 osu.Game/Configuration/SettingSourceAttribute.cs | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 4cc31e14ac..cfce615130 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -153,12 +153,8 @@ namespace osu.Game.Configuration
         }
 
         public static ICollection<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj)
-        {
-            var original = obj.GetSettingsSourceProperties().ToArray();
-
-            return original
-                   .OrderBy(attr => attr.Item1)
-                   .ToArray();
-        }
+            => obj.GetSettingsSourceProperties()
+                  .OrderBy(attr => attr.Item1)
+                  .ToArray();
     }
 }

From 41b43dd39a8b0b5e76a0f82e2d06b19ee633d696 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 28 Feb 2021 21:32:56 +0300
Subject: [PATCH 102/434] Add nested legacy-simulating coordinates container

---
 .../Skinning/Legacy/LegacySpinner.cs          | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index ec7ecb0d28..94b6a906d0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -127,5 +127,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             if (DrawableSpinner != null)
                 DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms;
         }
+
+        /// <summary>
+        /// A <see cref="Container"/> simulating osu!stable's absolute screen-space,
+        /// for perfect placements of legacy spinner components with legacy coordinates.
+        /// </summary>
+        protected class LegacyCoordinatesContainer : Container
+        {
+            /// <summary>
+            /// An offset that simulates stable's spinner top offset,
+            /// for positioning some legacy spinner components perfectly as in stable.
+            /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
+            /// </summary>
+            public const float SPINNER_TOP_OFFSET = 29f;
+
+            public LegacyCoordinatesContainer()
+            {
+                // legacy spinners relied heavily on absolute screen-space coordinate values.
+                // wrap everything in a container simulating absolute coords to preserve alignment
+                // as there are skins that depend on it.
+                Anchor = Anchor.Centre;
+                Origin = Anchor.Centre;
+                Size = new Vector2(640, 480);
+
+                // since legacy coordinates were on screen-space, they were accounting for the playfield shift offset.
+                // therefore cancel it from here.
+                Position = new Vector2(0, -8f);
+            }
+        }
     }
 }

From d528ef426fac4f30f380e35d12a2b4a99f59b69f Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 28 Feb 2021 21:41:11 +0300
Subject: [PATCH 103/434] Reposition legacy spinner components in-line with
 osu!stable

---
 .../Skinning/Legacy/LegacyOldStyleSpinner.cs  | 44 ++++++++-----------
 .../Skinning/Legacy/LegacySpinner.cs          | 40 +++++++++--------
 2 files changed, 39 insertions(+), 45 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 4e07cb60b3..7e9f73a89b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -33,39 +33,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         {
             spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
 
-            AddInternal(new Container
+            AddRangeInternal(new Drawable[]
             {
-                // the old-style spinner relied heavily on absolute screen-space coordinate values.
-                // wrap everything in a container simulating absolute coords to preserve alignment
-                // as there are skins that depend on it.
-                Width = 640,
-                Height = 480,
-                Anchor = Anchor.Centre,
-                Origin = Anchor.Centre,
-                Children = new Drawable[]
+                new Sprite
                 {
-                    new Sprite
-                    {
-                        Anchor = Anchor.Centre,
-                        Origin = Anchor.Centre,
-                        Texture = source.GetTexture("spinner-background"),
-                        Scale = new Vector2(SPRITE_SCALE)
-                    },
-                    disc = new Sprite
-                    {
-                        Anchor = Anchor.Centre,
-                        Origin = Anchor.Centre,
-                        Texture = source.GetTexture("spinner-circle"),
-                        Scale = new Vector2(SPRITE_SCALE)
-                    },
-                    metre = new Container
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Texture = source.GetTexture("spinner-background"),
+                    Scale = new Vector2(SPRITE_SCALE)
+                },
+                disc = new Sprite
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Texture = source.GetTexture("spinner-circle"),
+                    Scale = new Vector2(SPRITE_SCALE)
+                },
+                new LegacyCoordinatesContainer
+                {
+                    Child = metre = new Container
                     {
                         AutoSizeAxes = Axes.Both,
                         // this anchor makes no sense, but that's what stable uses.
                         Anchor = Anchor.TopLeft,
                         Origin = Anchor.TopLeft,
-                        // adjustment for stable (metre has additional offset)
-                        Margin = new MarginPadding { Top = 20 },
+                        Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET },
                         Masking = true,
                         Child = metreSprite = new Sprite
                         {
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 94b6a906d0..1f1fd1fbd9 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -30,27 +30,29 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            AddRangeInternal(new[]
+            AddInternal(new LegacyCoordinatesContainer
             {
-                spin = new Sprite
+                Depth = float.MinValue,
+                Children = new Drawable[]
                 {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    Depth = float.MinValue,
-                    Texture = source.GetTexture("spinner-spin"),
-                    Scale = new Vector2(SPRITE_SCALE),
-                    Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter
-                },
-                clear = new Sprite
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    Depth = float.MinValue,
-                    Alpha = 0,
-                    Texture = source.GetTexture("spinner-clear"),
-                    Scale = new Vector2(SPRITE_SCALE),
-                    Y = -60
-                },
+                    spin = new Sprite
+                    {
+                        Anchor = Anchor.TopCentre,
+                        Origin = Anchor.Centre,
+                        Texture = source.GetTexture("spinner-spin"),
+                        Scale = new Vector2(SPRITE_SCALE),
+                        Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335,
+                    },
+                    clear = new Sprite
+                    {
+                        Alpha = 0,
+                        Anchor = Anchor.TopCentre,
+                        Origin = Anchor.Centre,
+                        Texture = source.GetTexture("spinner-clear"),
+                        Scale = new Vector2(SPRITE_SCALE),
+                        Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115,
+                    },
+                }
             });
         }
 

From 97bb217830e2f2e28942bb04083d3682c0c31c31 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 1 Mar 2021 17:24:05 +0900
Subject: [PATCH 104/434] Fix test room playlist items not getting ids

---
 .../Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs    | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index 5e12156f3c..022c297ccd 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
             int currentScoreId = 0;
             int currentRoomId = 0;
+            int currentPlaylistItemId = 0;
 
             ((DummyAPIAccess)api).HandleRequest = req =>
             {
@@ -46,6 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
                         createdRoom.CopyFrom(createRoomRequest.Room);
                         createdRoom.RoomID.Value ??= currentRoomId++;
 
+                        for (int i = 0; i < createdRoom.Playlist.Count; i++)
+                            createdRoom.Playlist[i].ID = currentPlaylistItemId++;
+
                         rooms.Add(createdRoom);
                         createRoomRequest.TriggerSuccess(createdRoom);
                         break;

From f7e4cfa4d0dce04258aad29b03917048954b93c4 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 1 Mar 2021 17:24:32 +0900
Subject: [PATCH 105/434] Fix initial room settings not being returned
 correctly

---
 .../Multiplayer/TestMultiplayerClient.cs      | 28 +++++++++++++++----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 379bb758c5..67679b2659 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
         [Resolved]
         private IAPIProvider api { get; set; } = null!;
 
+        [Resolved]
+        private Room apiRoom { get; set; } = null!;
+
         public void Connect() => isConnected.Value = true;
 
         public void Disconnect() => isConnected.Value = false;
@@ -89,13 +92,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
         protected override Task<MultiplayerRoom> JoinRoom(long roomId)
         {
-            var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
+            Debug.Assert(apiRoom != null);
 
-            var room = new MultiplayerRoom(roomId);
-            room.Users.Add(user);
+            var user = new MultiplayerRoomUser(api.LocalUser.Value.Id)
+            {
+                User = api.LocalUser.Value
+            };
 
-            if (room.Users.Count == 1)
-                room.Host = user;
+            var room = new MultiplayerRoom(roomId)
+            {
+                Settings =
+                {
+                    Name = apiRoom.Name.Value,
+                    BeatmapID = apiRoom.Playlist.Last().BeatmapID,
+                    RulesetID = apiRoom.Playlist.Last().RulesetID,
+                    BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash,
+                    RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(),
+                    AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(),
+                    PlaylistItemId = apiRoom.Playlist.Last().ID
+                },
+                Users = { user },
+                Host = user
+            };
 
             return Task.FromResult(room);
         }

From 7adb33f40e352137e26b9cb82fc1ad675da881af Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 1 Mar 2021 17:24:54 +0900
Subject: [PATCH 106/434] Fix beatmap getting nulled due to failing web request

---
 .../Online/Multiplayer/MultiplayerClient.cs   | 25 ++++++++++++
 .../Multiplayer/StatefulMultiplayerClient.cs  | 38 ++++++++++---------
 .../Multiplayer/TestMultiplayerClient.cs      | 20 ++++++++++
 3 files changed, 65 insertions(+), 18 deletions(-)

diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 95d76f384f..4529dfd0a7 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -9,7 +9,9 @@ using System.Threading.Tasks;
 using Microsoft.AspNetCore.SignalR.Client;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
 using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
 using osu.Game.Online.Rooms;
 
 namespace osu.Game.Online.Multiplayer
@@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer
             return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
         }
 
+        protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
+        {
+            var tcs = new TaskCompletionSource<BeatmapSetInfo>();
+            var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
+
+            req.Success += res =>
+            {
+                if (cancellationToken.IsCancellationRequested)
+                {
+                    tcs.SetCanceled();
+                    return;
+                }
+
+                tcs.SetResult(res.ToBeatmapSet(Rulesets));
+            };
+
+            req.Failure += e => tcs.SetException(e);
+
+            API.Queue(req);
+
+            return tcs.Task;
+        }
+
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index bfd505fb19..73100be505 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -17,8 +17,6 @@ using osu.Framework.Logging;
 using osu.Game.Beatmaps;
 using osu.Game.Database;
 using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
-using osu.Game.Online.API.Requests.Responses;
 using osu.Game.Online.Rooms;
 using osu.Game.Online.Rooms.RoomStatuses;
 using osu.Game.Rulesets;
@@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer
         /// <summary>
         /// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
         /// </summary>
-        public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id);
+        public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
 
         /// <summary>
         /// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
@@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer
             }
         }
 
+        [Resolved]
+        protected IAPIProvider API { get; private set; } = null!;
+
+        [Resolved]
+        protected RulesetStore Rulesets { get; private set; } = null!;
+
         [Resolved]
         private UserLookupCache userLookupCache { get; set; } = null!;
 
-        [Resolved]
-        private IAPIProvider api { get; set; } = null!;
-
-        [Resolved]
-        private RulesetStore rulesets { get; set; } = null!;
-
         // Only exists for compatibility with old osu-server-spectator build.
         // Todo: Can be removed on 2021/02/26.
         private long defaultPlaylistItemId;
@@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer
 
             RoomUpdated?.Invoke();
 
-            var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
-            req.Success += res =>
+            GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() =>
             {
                 if (cancellationToken.IsCancellationRequested)
                     return;
 
-                updatePlaylist(settings, res);
-            };
-
-            api.Queue(req);
+                updatePlaylist(settings, set.Result);
+            }), TaskContinuationOptions.OnlyOnRanToCompletion);
         }, cancellationToken);
 
-        private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
+        private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet)
         {
             if (Room == null || !Room.Settings.Equals(settings))
                 return;
 
             Debug.Assert(apiRoom != null);
 
-            var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
             var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
             beatmap.MD5Hash = settings.BeatmapChecksum;
 
-            var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
+            var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance();
             var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
             var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
 
@@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer
             }
         }
 
+        /// <summary>
+        /// Retrieves a <see cref="BeatmapSetInfo"/> from an online source.
+        /// </summary>
+        /// <param name="beatmapId">The beatmap set ID.</param>
+        /// <param name="cancellationToken">A token to cancel the request.</param>
+        /// <returns>The <see cref="BeatmapSetInfo"/> retrieval task.</returns>
+        protected abstract Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
+
         /// <summary>
         /// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
         /// </summary>
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 67679b2659..6a901fc45b 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -3,12 +3,15 @@
 
 #nullable enable
 
+using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
 using osu.Game.Online.API;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Rooms;
@@ -28,6 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
         [Resolved]
         private Room apiRoom { get; set; } = null!;
 
+        [Resolved]
+        private BeatmapManager beatmaps { get; set; } = null!;
+
         public void Connect() => isConnected.Value = true;
 
         public void Disconnect() => isConnected.Value = false;
@@ -168,5 +174,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
             return ((IMultiplayerClient)this).LoadRequested();
         }
+
+        protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
+        {
+            Debug.Assert(Room != null);
+            Debug.Assert(apiRoom != null);
+
+            var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
+                      ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet;
+
+            if (set == null)
+                throw new InvalidOperationException("Beatmap not found.");
+
+            return Task.FromResult(set);
+        }
     }
 }

From 5cfaf1de1b6d72cbbf900d0667bf2c2e48e6f77c Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 1 Mar 2021 17:43:03 +0900
Subject: [PATCH 107/434] Fix duplicate ongoing operation tracker

---
 .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
index 2344ebea0e..8869718fd1 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs
@@ -3,12 +3,10 @@
 
 using System.Linq;
 using NUnit.Framework;
-using osu.Framework.Allocation;
 using osu.Framework.Screens;
 using osu.Framework.Testing;
 using osu.Game.Online.Rooms;
 using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.OnlinePlay;
 using osu.Game.Screens.OnlinePlay.Multiplayer;
 using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
 using osu.Game.Tests.Beatmaps;
@@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
     {
         private MultiplayerMatchSubScreen screen;
 
-        [Cached]
-        private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
-
         public TestSceneMultiplayerMatchSubScreen()
             : base(false)
         {

From fe54a51b5a9c5995db488e1c8873fd1691463a3a Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 1 Mar 2021 22:41:09 +0300
Subject: [PATCH 108/434] Remove `UserRanks` object and move to outer
 `country_rank` property

---
 osu.Game/Users/UserStatistics.cs | 17 +----------------
 1 file changed, 1 insertion(+), 16 deletions(-)

diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs
index 78e6f5a05a..dc926898fc 100644
--- a/osu.Game/Users/UserStatistics.cs
+++ b/osu.Game/Users/UserStatistics.cs
@@ -29,16 +29,9 @@ namespace osu.Game.Users
         [JsonProperty(@"global_rank")]
         public int? GlobalRank;
 
+        [JsonProperty(@"country_rank")]
         public int? CountryRank;
 
-        [JsonProperty(@"rank")]
-        private UserRanks ranks
-        {
-            // eventually that will also become an own json property instead of reading from a `rank` object.
-            // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53.
-            set => CountryRank = value.Country;
-        }
-
         // populated via User model, as that's where the data currently lives.
         public RankHistoryData RankHistory;
 
@@ -119,13 +112,5 @@ namespace osu.Game.Users
                 }
             }
         }
-
-#pragma warning disable 649
-        private struct UserRanks
-        {
-            [JsonProperty(@"country")]
-            public int? Country;
-        }
-#pragma warning restore 649
     }
 }

From 51a5652666d7d5e653fa28ddc0c74e23ddafe0f6 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 1 Mar 2021 22:42:53 +0300
Subject: [PATCH 109/434] Refetch tournament users on null country rank

---
 osu.Game.Tournament/TournamentGameBase.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index d506724017..2ee52c35aa 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -150,7 +150,9 @@ namespace osu.Game.Tournament
             {
                 foreach (var p in t.Players)
                 {
-                    if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null)
+                    if (string.IsNullOrEmpty(p.Username)
+                        || p.Statistics?.GlobalRank == null
+                        || p.Statistics?.CountryRank == null)
                     {
                         PopulateUser(p, immediate: true);
                         addedInfo = true;

From 2d3c3c18d4c1c3e1174079f2363a5d2e03b29c16 Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Mon, 1 Mar 2021 20:05:35 +0000
Subject: [PATCH 110/434] Bump SharpCompress from 0.27.1 to 0.28.1

Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.27.1 to 0.28.1.
- [Release notes](https://github.com/adamhathcock/sharpcompress/releases)
- [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.27.1...0.28.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 84a74502c2..4d086844e4 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -32,7 +32,7 @@
     <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.1" />
-    <PackageReference Include="SharpCompress" Version="0.27.1" />
+    <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
   </ItemGroup>
diff --git a/osu.iOS.props b/osu.iOS.props
index 2cea2e4b13..c0cfb7a96d 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -92,7 +92,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
-    <PackageReference Include="SharpCompress" Version="0.27.1" />
+    <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

From 9db37e62d8dc33bd19ef35861dab19b5f861af86 Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Mon, 1 Mar 2021 20:05:53 +0000
Subject: [PATCH 111/434] Bump
 Microsoft.AspNetCore.SignalR.Protocols.MessagePack

Bumps [Microsoft.AspNetCore.SignalR.Protocols.MessagePack](https://github.com/dotnet/aspnetcore) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/dotnet/aspnetcore/releases)
- [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.2...v5.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 84a74502c2..ca39c160a4 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -22,7 +22,7 @@
     <PackageReference Include="Humanizer" Version="2.8.26" />
     <PackageReference Include="MessagePack" Version="2.2.85" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.2" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 2cea2e4b13..c854ae7dff 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -80,7 +80,7 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="3.0.3" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
     <PackageReference Include="MessagePack" Version="1.7.3.7" />
     <PackageReference Include="MessagePack.Annotations" Version="2.2.85" />
   </ItemGroup>

From 2609b22d53626ff13206a88e70714b952ff5ff35 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 1 Mar 2021 23:07:25 +0300
Subject: [PATCH 112/434] Replace usage of `CurrentModeRank` in line with API
 change

---
 osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs       | 4 ++--
 .../Visual/Playlists/TestScenePlaylistsParticipantsList.cs   | 2 +-
 osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs         | 2 +-
 osu.Game/Users/User.cs                                       | 5 +----
 4 files changed, 5 insertions(+), 8 deletions(-)

diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
index 9bece39ca0..e8d9ff72af 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Online
                 Username = "flyte",
                 Id = 3103765,
                 IsOnline = true,
-                CurrentModeRank = 1111,
+                Statistics = new UserStatistics { GlobalRank = 1111 },
                 Country = new Country { FlagName = "JP" },
                 CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
             },
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online
                 Username = "peppy",
                 Id = 2,
                 IsOnline = false,
-                CurrentModeRank = 2222,
+                Statistics = new UserStatistics { GlobalRank = 2222 },
                 Country = new Country { FlagName = "AU" },
                 CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
                 IsSupporter = true,
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
index 8dd81e02e2..255f147ec9 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Playlists
                 Room.RecentParticipants.Add(new User
                 {
                     Username = "peppy",
-                    CurrentModeRank = 1234,
+                    Statistics = new UserStatistics { GlobalRank = 1234 },
                     Id = 2
                 });
             }
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index e6fe6ac749..0922ce5ecc 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
                     return unsorted.OrderByDescending(u => u.LastVisit).ToList();
 
                 case UserSortCriteria.Rank:
-                    return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList();
+                    return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList();
 
                 case UserSortCriteria.Username:
                     return unsorted.OrderBy(u => u.Username).ToList();
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 4a6fd540c7..4d537b91bd 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -72,9 +72,6 @@ namespace osu.Game.Users
         [JsonProperty(@"support_level")]
         public int SupportLevel;
 
-        [JsonProperty(@"current_mode_rank")]
-        public int? CurrentModeRank;
-
         [JsonProperty(@"is_gmt")]
         public bool IsGMT;
 
@@ -182,7 +179,7 @@ namespace osu.Game.Users
         private UserStatistics statistics;
 
         /// <summary>
-        /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> response).
+        /// User statistics for the requested ruleset (in the case of a <see cref="GetUserRequest"/> or <see cref="GetFriendsRequest"/> response).
         /// Otherwise empty.
         /// </summary>
         [JsonProperty(@"statistics")]

From d6925d09609c81bc8b8dc426d66440f7f25cedad Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Tue, 2 Mar 2021 00:43:44 +0000
Subject: [PATCH 113/434] Bump Moq from 4.16.0 to 4.16.1

Bumps [Moq](https://github.com/moq/moq4) from 4.16.0 to 4.16.1.
- [Release notes](https://github.com/moq/moq4/releases)
- [Changelog](https://github.com/moq/moq4/blob/main/CHANGELOG.md)
- [Commits](https://github.com/moq/moq4/compare/v4.16.0...v4.16.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 2 +-
 osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj         | 2 +-
 osu.Game.Tests/osu.Game.Tests.csproj                 | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index 19e36a63f1..543f2f35a7 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -71,7 +71,7 @@
   </ItemGroup>
   <ItemGroup Label="Package References">
     <PackageReference Include="DeepEqual" Version="2.0.0" />
-    <PackageReference Include="Moq" Version="4.16.0" />
+    <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index 67b2298f4c..e83bef4a95 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -45,7 +45,7 @@
   </ItemGroup>
   <ItemGroup Label="Package References">
     <PackageReference Include="DeepEqual" Version="2.0.0" />
-    <PackageReference Include="Moq" Version="4.16.0" />
+    <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 7e3868bd3b..877f41fbff 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -7,7 +7,7 @@
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
-    <PackageReference Include="Moq" Version="4.16.0" />
+    <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>

From b03efd69402995a6bc4ce62cf3b903ace5de396b Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Tue, 2 Mar 2021 00:43:45 +0000
Subject: [PATCH 114/434] Bump Microsoft.NET.Test.Sdk from 16.8.3 to 16.9.1

Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.8.3 to 16.9.1.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v16.8.3...v16.9.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 .../osu.Game.Rulesets.Catch.Tests.csproj                        | 2 +-
 .../osu.Game.Rulesets.Mania.Tests.csproj                        | 2 +-
 osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj  | 2 +-
 .../osu.Game.Rulesets.Taiko.Tests.csproj                        | 2 +-
 osu.Game.Tests/osu.Game.Tests.csproj                            | 2 +-
 osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj      | 2 +-
 6 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index bf3aba5859..728af5124e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,7 +2,7 @@
   <Import Project="..\osu.TestProject.props" />
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index fcc0cafefc..af16f39563 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,7 +2,7 @@
   <Import Project="..\osu.TestProject.props" />
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index b4c686ccea..3d2d1f3fec 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,7 +2,7 @@
   <Import Project="..\osu.TestProject.props" />
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 2b084f3bee..fa00922706 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,7 +2,7 @@
   <Import Project="..\osu.TestProject.props" />
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 7e3868bd3b..6c5ca937e2 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,7 +3,7 @@
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
     <PackageReference Include="DeepEqual" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
     <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 77ae06d89c..b20583dd7e 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -5,7 +5,7 @@
   </PropertyGroup>
   <ItemGroup Label="Package References">
     <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
   </ItemGroup>

From 7829a0636e5c021db48d058df16a6554313182d6 Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Tue, 2 Mar 2021 00:43:47 +0000
Subject: [PATCH 115/434] Bump Sentry from 3.0.1 to 3.0.7

Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 3.0.1 to 3.0.7.
- [Release notes](https://github.com/getsentry/sentry-dotnet/releases)
- [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-dotnet/compare/3.0.1...3.0.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 osu.Game/osu.Game.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 5ec7fb81fc..9916122a2a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -31,7 +31,7 @@
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="Sentry" Version="3.0.1" />
+    <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

From fa959291216feeb9e24174d74f9c3bc9a3882f36 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 2 Mar 2021 16:07:09 +0900
Subject: [PATCH 116/434] Remove easy to remove finalizers

---
 osu.Game/Database/DatabaseWriteUsage.cs | 5 -----
 osu.Game/Utils/SentryLogger.cs          | 5 -----
 2 files changed, 10 deletions(-)

diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs
index ddafd77066..84c39e3532 100644
--- a/osu.Game/Database/DatabaseWriteUsage.cs
+++ b/osu.Game/Database/DatabaseWriteUsage.cs
@@ -54,10 +54,5 @@ namespace osu.Game.Database
             Dispose(true);
             GC.SuppressFinalize(this);
         }
-
-        ~DatabaseWriteUsage()
-        {
-            Dispose(false);
-        }
     }
 }
diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs
index be9d01cde6..8f12760a6b 100644
--- a/osu.Game/Utils/SentryLogger.cs
+++ b/osu.Game/Utils/SentryLogger.cs
@@ -86,11 +86,6 @@ namespace osu.Game.Utils
 
         #region Disposal
 
-        ~SentryLogger()
-        {
-            Dispose(false);
-        }
-
         public void Dispose()
         {
             Dispose(true);

From c4ba045df158275175e0a84675106986ae96b946 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 2 Mar 2021 16:07:51 +0900
Subject: [PATCH 117/434] Add note about finalizers required for audio store
 clean-up

---
 osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 1 +
 osu.Game/Skinning/Skin.cs                           | 1 +
 2 files changed, 2 insertions(+)

diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index b31884d246..14aa3fe99a 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.UI
 
         ~DrawableRulesetDependencies()
         {
+            // required to potentially clean up sample store from audio hierarchy.
             Dispose(false);
         }
 
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index e8d84b49f9..6b435cff0f 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -36,6 +36,7 @@ namespace osu.Game.Skinning
 
         ~Skin()
         {
+            // required to potentially clean up sample store from audio hierarchy.
             Dispose(false);
         }
 

From 103dd4a6cea72ec399ac995acfe5270bd1da0de7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 2 Mar 2021 16:14:43 +0900
Subject: [PATCH 118/434] Remove WorkingBeatmap's finalizer

---
 osu.Game/Beatmaps/BeatmapManager.cs |  4 ++++
 osu.Game/Beatmaps/WorkingBeatmap.cs | 10 ----------
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 3c6a6ba302..d653e5386b 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -20,6 +20,7 @@ using osu.Framework.IO.Stores;
 using osu.Framework.Lists;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
+using osu.Framework.Statistics;
 using osu.Framework.Testing;
 using osu.Game.Beatmaps.Formats;
 using osu.Game.Database;
@@ -311,6 +312,9 @@ namespace osu.Game.Beatmaps
 
                 workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
 
+                // best effort; may be higher than expected.
+                GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
+
                 return working;
             }
         }
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index aab8ff6bd6..f7f276230f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -12,7 +12,6 @@ using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Logging;
-using osu.Framework.Statistics;
 using osu.Framework.Testing;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
@@ -34,8 +33,6 @@ namespace osu.Game.Beatmaps
 
         protected AudioManager AudioManager { get; }
 
-        private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
-
         protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
         {
             AudioManager = audioManager;
@@ -47,8 +44,6 @@ namespace osu.Game.Beatmaps
             waveform = new RecyclableLazy<Waveform>(GetWaveform);
             storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
             skin = new RecyclableLazy<ISkin>(GetSkin);
-
-            total_count.Value++;
         }
 
         protected virtual Track GetVirtualTrack(double emptyLength = 0)
@@ -331,11 +326,6 @@ namespace osu.Game.Beatmaps
         protected virtual ISkin GetSkin() => new DefaultSkin();
         private readonly RecyclableLazy<ISkin> skin;
 
-        ~WorkingBeatmap()
-        {
-            total_count.Value--;
-        }
-
         public class RecyclableLazy<T>
         {
             private Lazy<T> lazy;

From 6372a0265af98f48afa15d4c4499a1a33250db6c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 2 Mar 2021 17:44:56 +0900
Subject: [PATCH 119/434] Fix confine mode dropdown becoming visible again
 after filtering

Changes from a hidden to a disabled state, with a tooltip explaining
why.

Closes #11851.
---
 .../Settings/Sections/Input/MouseSettings.cs     | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 455e13711d..768a18cca0 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -68,7 +68,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             };
 
             windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
-            windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true);
+            windowMode.BindValueChanged(mode =>
+            {
+                var isFullscreen = mode.NewValue == WindowMode.Fullscreen;
+
+                if (isFullscreen)
+                {
+                    confineMouseModeSetting.Current.Disabled = true;
+                    confineMouseModeSetting.TooltipText = "Not applicable in full screen mode";
+                }
+                else
+                {
+                    confineMouseModeSetting.Current.Disabled = false;
+                    confineMouseModeSetting.TooltipText = string.Empty;
+                }
+            }, true);
 
             if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
             {

From 0300a554476c72fb0e07774350f0fc79687718c2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 2 Mar 2021 18:00:50 +0900
Subject: [PATCH 120/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 5d83bb9583..c428cd2546 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.226.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.302.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9916122a2a..2528292e17 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.302.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index b4f981162a..56a24bea12 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.226.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.302.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -91,7 +91,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.226.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.302.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 30ff0b83c199a20ddcd2e86beb643842f1263daf Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 2 Mar 2021 19:06:21 +0900
Subject: [PATCH 121/434] Fix test failures due to unpopulated room

---
 .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs  | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
index 2e8c834c65..7775c2bd24 100644
--- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs
@@ -7,8 +7,10 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
 using osu.Game.Screens.OnlinePlay;
 using osu.Game.Screens.OnlinePlay.Lounge.Components;
+using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual.Multiplayer
 {
@@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
             RoomManager.Schedule(() => RoomManager.PartRoom());
 
             if (joinRoom)
+            {
+                Room.Name.Value = "test name";
+                Room.Playlist.Add(new PlaylistItem
+                {
+                    Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+                    Ruleset = { Value = Ruleset.Value }
+                });
+
                 RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
+            }
         });
 
         public override void SetUpSteps()

From 711cf3e5111e46f293b8aba2216c082378944eb3 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 17:25:36 +0300
Subject: [PATCH 122/434] Add mobile logs location to issue templates

---
 .github/ISSUE_TEMPLATE/01-bug-issues.md   | 2 ++
 .github/ISSUE_TEMPLATE/02-crash-issues.md | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 0b80ce44dd..6050036cbf 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -13,4 +13,6 @@ about: Issues regarding encountered bugs.
 *please attach logs here, which are located at:*
 - `%AppData%/osu/logs` *(on Windows),*
 - `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
+- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
 -->
diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
index ada8de73c0..04170312d1 100644
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md
@@ -13,6 +13,8 @@ about: Issues regarding crashes or permanent freezes.
 *please attach logs here, which are located at:*
 - `%AppData%/osu/logs` *(on Windows),*
 - `~/.local/share/osu/logs` *(on Linux & macOS).*
+- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
+- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
 -->
 
 **Computer Specifications:** 

From 40a28367c63a2cf7e3c87eeccaf617b1f25564a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 18:50:33 +0100
Subject: [PATCH 123/434] Fix restore-to-default buttons never showing if
 initially hidden

---
 osu.Game/Overlays/Settings/SettingsItem.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index 4cb8d7f83c..c5890a6fbb 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -147,6 +147,7 @@ namespace osu.Game.Overlays.Settings
                 RelativeSizeAxes = Axes.Y;
                 Width = SettingsPanel.CONTENT_MARGINS;
                 Alpha = 0f;
+                AlwaysPresent = true;
             }
 
             [BackgroundDependencyLoader]

From 3b125a26a863e61d20a9a5018ab10383c2486611 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 19:18:01 +0100
Subject: [PATCH 124/434] Add test coverage

---
 .../Visual/Settings/TestSceneSettingsItem.cs  | 43 +++++++++++++++++++
 osu.Game/Overlays/Settings/SettingsItem.cs    |  2 +-
 2 files changed, 44 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
new file mode 100644
index 0000000000..8f1c17ed29
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+    [TestFixture]
+    public class TestSceneSettingsItem : OsuTestScene
+    {
+        [Test]
+        public void TestRestoreDefaultValueButtonVisibility()
+        {
+            TestSettingsTextBox textBox = null;
+
+            AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
+            {
+                Current = new Bindable<string>
+                {
+                    Default = "test",
+                    Value = "test"
+                }
+            });
+            AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+
+            AddStep("change value from default", () => textBox.Current.Value = "non-default");
+            AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
+
+            AddStep("restore default", () => textBox.Current.SetDefault());
+            AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
+        }
+
+        private class TestSettingsTextBox : SettingsTextBox
+        {
+            public new Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton>().Single();
+        }
+    }
+}
diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index c5890a6fbb..8631b8ac7b 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Settings
                 labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
         }
 
-        private class RestoreDefaultValueButton : Container, IHasTooltip
+        protected internal class RestoreDefaultValueButton : Container, IHasTooltip
         {
             private Bindable<T> bindable;
 

From 26736d990f792f15bd0c92f7f5a100a8f800816d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 19:42:47 +0100
Subject: [PATCH 125/434] Enable filter parsing extensibility

---
 osu.Game/Screens/Select/FilterQueryParser.cs | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 4b6b3be45c..3f1b80ee1c 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select
     internal static class FilterQueryParser
     {
         private static readonly Regex query_syntax_regex = new Regex(
-            @"\b(?<key>stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
+            @"\b(?<key>\w+)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
             RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
         internal static void ApplyQueries(FilterCriteria criteria, string query)
@@ -22,15 +22,14 @@ namespace osu.Game.Screens.Select
                 var op = match.Groups["op"].Value;
                 var value = match.Groups["value"].Value;
 
-                parseKeywordCriteria(criteria, key, value, op);
-
-                query = query.Replace(match.ToString(), "");
+                if (tryParseKeywordCriteria(criteria, key, value, op))
+                    query = query.Replace(match.ToString(), "");
             }
 
             criteria.SearchText = query;
         }
 
-        private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
+        private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
         {
             switch (key)
             {
@@ -75,7 +74,12 @@ namespace osu.Game.Screens.Select
                 case "artist":
                     updateCriteriaText(ref criteria.Artist, op, value);
                     break;
+
+                default:
+                    return false;
             }
+
+            return true;
         }
 
         private static int getLengthScale(string value) =>

From e46543a4a924d6bab3b7ef67fa8d3d992d6504bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 19:56:36 +0100
Subject: [PATCH 126/434] Constrain operator parsing better

---
 .../Filtering/FilterQueryParserTest.cs        |   9 ++
 osu.Game/Screens/Select/Filter/Operator.cs    |  17 +++
 osu.Game/Screens/Select/FilterQueryParser.cs  | 131 ++++++++++--------
 3 files changed, 99 insertions(+), 58 deletions(-)
 create mode 100644 osu.Game/Screens/Select/Filter/Operator.cs

diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index d15682b1eb..e121cb835c 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -194,5 +194,14 @@ namespace osu.Game.Tests.NonVisual.Filtering
             Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
             Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
         }
+
+        [Test]
+        public void TestOperatorParsing()
+        {
+            const string query = "artist=><something";
+            var filterCriteria = new FilterCriteria();
+            FilterQueryParser.ApplyQueries(filterCriteria, query);
+            Assert.AreEqual("><something", filterCriteria.Artist.SearchTerm);
+        }
     }
 }
diff --git a/osu.Game/Screens/Select/Filter/Operator.cs b/osu.Game/Screens/Select/Filter/Operator.cs
new file mode 100644
index 0000000000..706daf631f
--- /dev/null
+++ b/osu.Game/Screens/Select/Filter/Operator.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.Select.Filter
+{
+    /// <summary>
+    /// Defines logical operators that can be used in the song select search box keyword filters.
+    /// </summary>
+    public enum Operator
+    {
+        Less,
+        LessOrEqual,
+        Equal,
+        GreaterOrEqual,
+        Greater
+    }
+}
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 3f1b80ee1c..d2d33b13f5 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -5,13 +5,14 @@ using System;
 using System.Globalization;
 using System.Text.RegularExpressions;
 using osu.Game.Beatmaps;
+using osu.Game.Screens.Select.Filter;
 
 namespace osu.Game.Screens.Select
 {
     internal static class FilterQueryParser
     {
         private static readonly Regex query_syntax_regex = new Regex(
-            @"\b(?<key>\w+)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
+            @"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
             RegexOptions.Compiled | RegexOptions.IgnoreCase);
 
         internal static void ApplyQueries(FilterCriteria criteria, string query)
@@ -19,7 +20,7 @@ namespace osu.Game.Screens.Select
             foreach (Match match in query_syntax_regex.Matches(query))
             {
                 var key = match.Groups["key"].Value.ToLower();
-                var op = match.Groups["op"].Value;
+                var op = parseOperator(match.Groups["op"].Value);
                 var value = match.Groups["value"].Value;
 
                 if (tryParseKeywordCriteria(criteria, key, value, op))
@@ -29,57 +30,72 @@ namespace osu.Game.Screens.Select
             criteria.SearchText = query;
         }
 
-        private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
+        private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op)
         {
             switch (key)
             {
                 case "stars" when parseFloatWithPoint(value, out var stars):
-                    updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
-                    break;
+                    return updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
 
                 case "ar" when parseFloatWithPoint(value, out var ar):
-                    updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
-                    break;
+                    return updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
 
                 case "dr" when parseFloatWithPoint(value, out var dr):
                 case "hp" when parseFloatWithPoint(value, out dr):
-                    updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
-                    break;
+                    return updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
 
                 case "cs" when parseFloatWithPoint(value, out var cs):
-                    updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
-                    break;
+                    return updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
 
                 case "bpm" when parseDoubleWithPoint(value, out var bpm):
-                    updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
-                    break;
+                    return updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
 
                 case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
                     var scale = getLengthScale(value);
-                    updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
-                    break;
+                    return updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
 
                 case "divisor" when parseInt(value, out var divisor):
-                    updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
-                    break;
+                    return updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
 
                 case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
-                    updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
-                    break;
+                    return updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
 
                 case "creator":
-                    updateCriteriaText(ref criteria.Creator, op, value);
-                    break;
+                    return updateCriteriaText(ref criteria.Creator, op, value);
 
                 case "artist":
-                    updateCriteriaText(ref criteria.Artist, op, value);
-                    break;
+                    return updateCriteriaText(ref criteria.Artist, op, value);
 
                 default:
                     return false;
             }
+        }
 
-            return true;
+        private static Operator parseOperator(string value)
+        {
+            switch (value)
+            {
+                case "=":
+                case ":":
+                    return Operator.Equal;
+
+                case "<":
+                    return Operator.Less;
+
+                case "<=":
+                case "<:":
+                    return Operator.LessOrEqual;
+
+                case ">":
+                    return Operator.Greater;
+
+                case ">=":
+                case ">:":
+                    return Operator.GreaterOrEqual;
+
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}");
+            }
         }
 
         private static int getLengthScale(string value) =>
@@ -97,120 +113,119 @@ namespace osu.Game.Screens.Select
         private static bool parseInt(string value, out int result) =>
             int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
 
-        private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
+        private static bool updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
         {
             switch (op)
             {
-                case "=":
-                case ":":
+                case Operator.Equal:
                     textFilter.SearchTerm = value.Trim('"');
-                    break;
+                    return true;
+
+                default:
+                    return false;
             }
         }
 
-        private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
+        private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
         {
             switch (op)
             {
                 default:
-                    return;
+                    return false;
 
-                case "=":
-                case ":":
+                case Operator.Equal:
                     range.Min = value - tolerance;
                     range.Max = value + tolerance;
                     break;
 
-                case ">":
+                case Operator.Greater:
                     range.Min = value + tolerance;
                     break;
 
-                case ">=":
-                case ">:":
+                case Operator.GreaterOrEqual:
                     range.Min = value - tolerance;
                     break;
 
-                case "<":
+                case Operator.Less:
                     range.Max = value - tolerance;
                     break;
 
-                case "<=":
-                case "<:":
+                case Operator.LessOrEqual:
                     range.Max = value + tolerance;
                     break;
             }
+
+            return true;
         }
 
-        private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
+        private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
         {
             switch (op)
             {
                 default:
-                    return;
+                    return false;
 
-                case "=":
-                case ":":
+                case Operator.Equal:
                     range.Min = value - tolerance;
                     range.Max = value + tolerance;
                     break;
 
-                case ">":
+                case Operator.Greater:
                     range.Min = value + tolerance;
                     break;
 
-                case ">=":
-                case ">:":
+                case Operator.GreaterOrEqual:
                     range.Min = value - tolerance;
                     break;
 
-                case "<":
+                case Operator.Less:
                     range.Max = value - tolerance;
                     break;
 
-                case "<=":
-                case "<:":
+                case Operator.LessOrEqual:
                     range.Max = value + tolerance;
                     break;
             }
+
+            return true;
         }
 
-        private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
+        private static bool updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
             where T : struct
         {
             switch (op)
             {
                 default:
-                    return;
+                    return false;
 
-                case "=":
-                case ":":
+                case Operator.Equal:
                     range.IsLowerInclusive = range.IsUpperInclusive = true;
                     range.Min = value;
                     range.Max = value;
                     break;
 
-                case ">":
+                case Operator.Greater:
                     range.IsLowerInclusive = false;
                     range.Min = value;
                     break;
 
-                case ">=":
-                case ">:":
+                case Operator.GreaterOrEqual:
                     range.IsLowerInclusive = true;
                     range.Min = value;
                     break;
 
-                case "<":
+                case Operator.Less:
                     range.IsUpperInclusive = false;
                     range.Max = value;
                     break;
 
-                case "<=":
-                case "<:":
+                case Operator.LessOrEqual:
                     range.IsUpperInclusive = true;
                     range.Max = value;
                     break;
             }
+
+            return true;
         }
     }
 }

From 14e249a13405e834a7ea90b32cc3e8246efc37be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 20:07:11 +0100
Subject: [PATCH 127/434] Add ruleset interface for extending filter criteria

---
 .../Rulesets/Filter/IRulesetFilterCriteria.cs | 44 +++++++++++++++++++
 osu.Game/Rulesets/Ruleset.cs                  |  7 +++
 2 files changed, 51 insertions(+)
 create mode 100644 osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs

diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
new file mode 100644
index 0000000000..a83f87d72b
--- /dev/null
+++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
+
+namespace osu.Game.Rulesets.Filter
+{
+    /// <summary>
+    /// Allows for extending the beatmap filtering capabilities of song select (as implemented in <see cref="FilterCriteria"/>)
+    /// with ruleset-specific criteria.
+    /// </summary>
+    public interface IRulesetFilterCriteria
+    {
+        /// <summary>
+        /// Checks whether the supplied <paramref name="beatmap"/> satisfies ruleset-specific custom criteria,
+        /// in addition to the ones mandated by song select.
+        /// </summary>
+        /// <param name="beatmap">The beatmap to test the criteria against.</param>
+        /// <returns>
+        /// <c>true</c> if the beatmap matches the ruleset-specific custom filtering criteria,
+        /// <c>false</c> otherwise.
+        /// </returns>
+        bool Matches(BeatmapInfo beatmap);
+
+        /// <summary>
+        /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box.
+        /// The format of the criterion is:
+        /// <code>
+        /// {key}{op}{value}
+        /// </code>
+        /// </summary>
+        /// <param name="key">The key (name) of the criterion.</param>
+        /// <param name="op">The operator in the criterion.</param>
+        /// <param name="value">The value of the criterion.</param>
+        /// <returns>
+        /// <c>true</c> if the keyword criterion is valid, <c>false</c> if it has been ignored.
+        /// Valid criteria are stripped from <see cref="FilterCriteria.SearchText"/>,
+        /// while ignored criteria are included in <see cref="FilterCriteria.SearchText"/>.
+        /// </returns>
+        bool TryParseCustomKeywordCriteria(string key, Operator op, string value);
+    }
+}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index dbc2bd4d01..38d30a2e31 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -26,6 +26,7 @@ using JetBrains.Annotations;
 using osu.Framework.Extensions;
 using osu.Framework.Extensions.EnumExtensions;
 using osu.Framework.Testing;
+using osu.Game.Rulesets.Filter;
 using osu.Game.Screens.Ranking.Statistics;
 
 namespace osu.Game.Rulesets
@@ -306,5 +307,11 @@ namespace osu.Game.Rulesets
         /// <param name="result">The result type to get the name for.</param>
         /// <returns>The display name.</returns>
         public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
+
+        /// <summary>
+        /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
+        /// </summary>
+        [CanBeNull]
+        public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null;
     }
 }

From c375be6b07b7d4bc19d29683c61a9f4da6529d52 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 20:10:03 +0100
Subject: [PATCH 128/434] Instantiate ruleset criteria

---
 osu.Game/Screens/Select/FilterControl.cs  | 8 ++++++++
 osu.Game/Screens/Select/FilterCriteria.cs | 4 ++++
 2 files changed, 12 insertions(+)

diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index eafd8a87d1..983928ac51 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Diagnostics;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -34,8 +35,13 @@ namespace osu.Game.Screens.Select
 
         private Bindable<GroupMode> groupMode;
 
+        [Resolved]
+        private RulesetStore rulesets { get; set; }
+
         public FilterCriteria CreateCriteria()
         {
+            Debug.Assert(ruleset.Value.ID != null);
+
             var query = searchTextBox.Text;
 
             var criteria = new FilterCriteria
@@ -53,6 +59,8 @@ namespace osu.Game.Screens.Select
             if (!maximumStars.IsDefault)
                 criteria.UserStarDifficulty.Max = maximumStars.Value;
 
+            criteria.RulesetCriteria = rulesets.GetRuleset(ruleset.Value.ID.Value).CreateInstance().CreateRulesetFilterCriteria();
+
             FilterQueryParser.ApplyQueries(criteria, query);
             return criteria;
         }
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index 7bddb3e51b..208048380a 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -8,6 +8,7 @@ using JetBrains.Annotations;
 using osu.Game.Beatmaps;
 using osu.Game.Collections;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Filter;
 using osu.Game.Screens.Select.Filter;
 
 namespace osu.Game.Screens.Select
@@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select
         [CanBeNull]
         public BeatmapCollection Collection;
 
+        [CanBeNull]
+        public IRulesetFilterCriteria RulesetCriteria { get; set; }
+
         public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
             where T : struct
         {

From 42c3309d4918db8044c312d28f3efbc7422caae1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 20:11:21 +0100
Subject: [PATCH 129/434] Use ruleset criteria in parsing and filtering

---
 osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++
 osu.Game/Screens/Select/FilterQueryParser.cs        | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 1aab50037a..521b90202d 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel
             if (match)
                 match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
 
+            if (match && criteria.RulesetCriteria != null)
+                match &= criteria.RulesetCriteria.Matches(Beatmap);
+
             Filtered.Value = !match;
         }
 
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index d2d33b13f5..c81a72d938 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Screens.Select
                     return updateCriteriaText(ref criteria.Artist, op, value);
 
                 default:
-                    return false;
+                    return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
             }
         }
 

From bf72f9ad1e988f14dbd8ca5b87f55c64b52d9c5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 20:22:56 +0100
Subject: [PATCH 130/434] Add tests for custom parsing logic

---
 .../Filtering/FilterQueryParserTest.cs        | 40 +++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index e121cb835c..d835e58b29 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -4,7 +4,9 @@
 using System;
 using NUnit.Framework;
 using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Filter;
 using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Filter;
 
 namespace osu.Game.Tests.NonVisual.Filtering
 {
@@ -203,5 +205,43 @@ namespace osu.Game.Tests.NonVisual.Filtering
             FilterQueryParser.ApplyQueries(filterCriteria, query);
             Assert.AreEqual("><something", filterCriteria.Artist.SearchTerm);
         }
+
+        [Test]
+        public void TestUnrecognisedKeywordIsIgnored()
+        {
+            const string query = "unrecognised=keyword";
+            var filterCriteria = new FilterCriteria();
+            FilterQueryParser.ApplyQueries(filterCriteria, query);
+            Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText);
+        }
+
+        [Test]
+        public void TestCustomKeywordIsParsed()
+        {
+            var customCriteria = new CustomFilterCriteria();
+            const string query = "custom=readme unrecognised=keyword";
+            var filterCriteria = new FilterCriteria { RulesetCriteria = customCriteria };
+            FilterQueryParser.ApplyQueries(filterCriteria, query);
+            Assert.AreEqual("readme", customCriteria.CustomValue);
+            Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
+        }
+
+        private class CustomFilterCriteria : IRulesetFilterCriteria
+        {
+            public string CustomValue { get; set; }
+
+            public bool Matches(BeatmapInfo beatmap) => true;
+
+            public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
+            {
+                if (key == "custom" && op == Operator.Equal)
+                {
+                    CustomValue = value;
+                    return true;
+                }
+
+                return false;
+            }
+        }
     }
 }

From faf5fbf49b5a940b41ba3e7b1336fbefd868895a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Tue, 2 Mar 2021 20:27:50 +0100
Subject: [PATCH 131/434] Add tests for custom matching logic

---
 .../NonVisual/Filtering/FilterMatchingTest.cs | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
index 24a0a662ba..8ff2743b6a 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -4,8 +4,10 @@
 using NUnit.Framework;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Filter;
 using osu.Game.Screens.Select;
 using osu.Game.Screens.Select.Carousel;
+using osu.Game.Screens.Select.Filter;
 
 namespace osu.Game.Tests.NonVisual.Filtering
 {
@@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
 
             Assert.AreEqual(filtered, carouselItem.Filtered.Value);
         }
+
+        [Test]
+        public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria)
+        {
+            var beatmap = getExampleBeatmap();
+
+            var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null;
+            var criteria = new FilterCriteria { RulesetCriteria = customCriteria };
+            var carouselItem = new CarouselBeatmap(beatmap);
+            carouselItem.Filter(criteria);
+
+            Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value);
+        }
+
+        private class CustomCriteria : IRulesetFilterCriteria
+        {
+            private readonly bool match;
+
+            public CustomCriteria(bool shouldMatch)
+            {
+                match = shouldMatch;
+            }
+
+            public bool Matches(BeatmapInfo beatmap) => match;
+            public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
+        }
     }
 }

From 6e75ebbb06fb6bf394e257bc44877b3f7171923f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:02:01 +0900
Subject: [PATCH 132/434] Add interface to handle local beatmap presentation
 logic

---
 osu.Game/Screens/IHandlePresentBeatmap.cs | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)
 create mode 100644 osu.Game/Screens/IHandlePresentBeatmap.cs

diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs
new file mode 100644
index 0000000000..b94df630ef
--- /dev/null
+++ b/osu.Game/Screens/IHandlePresentBeatmap.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+
+namespace osu.Game.Screens
+{
+    /// <summary>
+    /// Denotes a screen which can handle beatmap / ruleset selection via local logic.
+    /// This is used in the <see cref="OsuGame.PresentBeatmap"/> flow to handle cases which require custom logic,
+    /// for instance, if a lease is held on the Beatmap.
+    /// </summary>
+    public interface IHandlePresentBeatmap
+    {
+        /// <summary>
+        /// Invoked with a requested beatmap / ruleset for selection.
+        /// </summary>
+        /// <param name="beatmap">The beatmap to be selected.</param>
+        /// <param name="ruleset">The ruleset to be selected.</param>
+        public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset);
+    }
+}

From 36e1fb6da80a1416900d07ae9c69c9b12e8876ff Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:04:00 +0900
Subject: [PATCH 133/434] Add flow to allow MatchSubScreen to handle beatmap
 presentation locally

---
 osu.Game/OsuGame.cs                           | 13 ++++++++++---
 osu.Game/PerformFromMenuRunner.cs             |  7 +------
 .../Multiplayer/MultiplayerMatchSongSelect.cs | 19 +++++++++++++++++++
 .../Multiplayer/MultiplayerMatchSubScreen.cs  | 12 +++++++++++-
 4 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 771bcd2310..1e0cb587e9 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -381,9 +381,16 @@ namespace osu.Game
                                 ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value))
                                 ?? beatmaps.First();
 
-                Ruleset.Value = selection.Ruleset;
-                Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
-            }, validScreens: new[] { typeof(SongSelect) });
+                if (screen is IHandlePresentBeatmap presentableScreen)
+                {
+                    presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset);
+                }
+                else
+                {
+                    Ruleset.Value = selection.Ruleset;
+                    Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
+                }
+            }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) });
         }
 
         /// <summary>
diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs
index fe75a3a607..6f979b8dc8 100644
--- a/osu.Game/PerformFromMenuRunner.cs
+++ b/osu.Game/PerformFromMenuRunner.cs
@@ -5,11 +5,9 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
-using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
-using osu.Game.Beatmaps;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Dialog;
 using osu.Game.Overlays.Notifications;
@@ -30,9 +28,6 @@ namespace osu.Game
         [Resolved]
         private DialogOverlay dialogOverlay { get; set; }
 
-        [Resolved]
-        private IBindable<WorkingBeatmap> beatmap { get; set; }
-
         [Resolved(canBeNull: true)]
         private OsuGame game { get; set; }
 
@@ -90,7 +85,7 @@ namespace osu.Game
             var type = current.GetType();
 
             // check if we are already at a valid target screen.
-            if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
+            if (validScreens.Any(t => t.IsAssignableFrom(type)))
             {
                 finalAction(current);
                 Cancel();
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
index f17d97c3fd..c9f0f6de90 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs
@@ -4,9 +4,11 @@
 using osu.Framework.Allocation;
 using osu.Framework.Logging;
 using osu.Framework.Screens;
+using osu.Game.Beatmaps;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.Select;
 
@@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
         private LoadingLayer loadingLayer;
 
+        /// <summary>
+        /// Construct a new instance of multiplayer song select.
+        /// </summary>
+        /// <param name="beatmap">An optional initial beatmap selection to perform.</param>
+        /// <param name="ruleset">An optional initial ruleset selection to perform.</param>
+        public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
+        {
+            if (beatmap != null || ruleset != null)
+            {
+                Schedule(() =>
+                {
+                    if (beatmap != null) Beatmap.Value = beatmap;
+                    if (ruleset != null) Ruleset.Value = ruleset;
+                });
+            }
+        }
+
         [BackgroundDependencyLoader]
         private void load()
         {
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 4fbea4e3be..06d83e495c 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -12,9 +12,11 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
+using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Rooms;
+using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.OnlinePlay.Components;
 using osu.Game.Screens.OnlinePlay.Match;
@@ -29,7 +31,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa
 namespace osu.Game.Screens.OnlinePlay.Multiplayer
 {
     [Cached]
-    public class MultiplayerMatchSubScreen : RoomSubScreen
+    public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap
     {
         public override string Title { get; }
 
@@ -394,5 +396,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
             modSettingChangeTracker?.Dispose();
         }
+
+        public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
+        {
+            if (!this.IsCurrentScreen())
+                return;
+
+            this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset));
+        }
     }
 }

From fcea900a5327cc3d421c7332ed001026f6521388 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:06:39 +0900
Subject: [PATCH 134/434] Move main menu (song select) presentation logic to a
 local implementation

Reduces cross-dependencies between OsuGame and MainMenu.
---
 osu.Game/OsuGame.cs               |  4 ----
 osu.Game/Screens/Menu/MainMenu.cs | 18 +++++++++++++-----
 2 files changed, 13 insertions(+), 9 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1e0cb587e9..6f760a1aa7 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -361,10 +361,6 @@ namespace osu.Game
 
             PerformFromScreen(screen =>
             {
-                // we might already be at song select, so a check is required before performing the load to solo.
-                if (screen is MainMenu)
-                    menuScreen.LoadToSolo();
-
                 // we might even already be at the song
                 if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true))
                     return;
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 424e6d2cd5..baeb86c976 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -9,12 +9,14 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Platform;
 using osu.Framework.Screens;
+using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.IO;
 using osu.Game.Online.API;
 using osu.Game.Overlays;
+using osu.Game.Rulesets;
 using osu.Game.Screens.Backgrounds;
 using osu.Game.Screens.Edit;
 using osu.Game.Screens.OnlinePlay.Multiplayer;
@@ -23,7 +25,7 @@ using osu.Game.Screens.Select;
 
 namespace osu.Game.Screens.Menu
 {
-    public class MainMenu : OsuScreen
+    public class MainMenu : OsuScreen, IHandlePresentBeatmap
     {
         public const float FADE_IN_DURATION = 300;
 
@@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu
                                 Beatmap.SetDefault();
                                 this.Push(new Editor());
                             },
-                            OnSolo = onSolo,
+                            OnSolo = loadSoloSongSelect,
                             OnMultiplayer = () => this.Push(new Multiplayer()),
                             OnPlaylists = () => this.Push(new Playlists()),
                             OnExit = confirmAndExit,
@@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu
                 LoadComponentAsync(songSelect = new PlaySongSelect());
         }
 
-        public void LoadToSolo() => Schedule(onSolo);
-
-        private void onSolo() => this.Push(consumeSongSelect());
+        private void loadSoloSongSelect() => this.Push(consumeSongSelect());
 
         private Screen consumeSongSelect()
         {
@@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu
             this.FadeOut(3000);
             return base.OnExiting(next);
         }
+
+        public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
+        {
+            Beatmap.Value = beatmap;
+            Ruleset.Value = ruleset;
+
+            Schedule(loadSoloSongSelect);
+        }
     }
 }

From 7c5904008247a113525396f9c4e1603ef470a464 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:17:06 +0900
Subject: [PATCH 135/434] Re-present even when already the current beatmap

This feels better and closer to what a user would expect.
---
 osu.Game/OsuGame.cs | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 6f760a1aa7..7db85d0d66 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -361,10 +361,6 @@ namespace osu.Game
 
             PerformFromScreen(screen =>
             {
-                // we might even already be at the song
-                if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true))
-                    return;
-
                 // Find beatmaps that match our predicate.
                 var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
 

From 7dce9b04fa0a7870d7576c803c8130a095124484 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:50:45 +0900
Subject: [PATCH 136/434] Add a more basic ConfirmDialog implementation

---
 osu.Game/Overlays/Dialog/ConfirmDialog.cs  | 45 ++++++++++++++++++++++
 osu.Game/Screens/Menu/ConfirmExitDialog.cs | 26 +++----------
 2 files changed, 50 insertions(+), 21 deletions(-)
 create mode 100644 osu.Game/Overlays/Dialog/ConfirmDialog.cs

diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
new file mode 100644
index 0000000000..6f160daf97
--- /dev/null
+++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics.Sprites;
+
+namespace osu.Game.Overlays.Dialog
+{
+    /// <summary>
+    /// A dialog which confirms a user action.
+    /// </summary>
+    public class ConfirmDialog : PopupDialog
+    {
+        protected PopupDialogOkButton ButtonConfirm;
+        protected PopupDialogCancelButton ButtonCancel;
+
+        /// <summary>
+        /// Construct a new dialog.
+        /// </summary>
+        /// <param name="description">The description of the action to be displayed to the user.</param>
+        /// <param name="onConfirm">An action to perform on confirmation.</param>
+        /// <param name="onCancel">An optional action to perform on cancel.</param>
+        public ConfirmDialog(string description, Action onConfirm, Action onCancel = null)
+        {
+            HeaderText = $"Are you sure you want to {description}?";
+            BodyText = "Last chance to back out.";
+
+            Icon = FontAwesome.Solid.ExclamationTriangle;
+
+            Buttons = new PopupDialogButton[]
+            {
+                ButtonConfirm = new PopupDialogOkButton
+                {
+                    Text = @"Yes",
+                    Action = onConfirm
+                },
+                ButtonCancel = new PopupDialogCancelButton
+                {
+                    Text = @"Cancel",
+                    Action = onCancel
+                },
+            };
+        }
+    }
+}
diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
index d120eb21a8..41cc7b480c 100644
--- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs
+++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
@@ -2,33 +2,17 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using osu.Framework.Graphics.Sprites;
 using osu.Game.Overlays.Dialog;
 
 namespace osu.Game.Screens.Menu
 {
-    public class ConfirmExitDialog : PopupDialog
+    public class ConfirmExitDialog : ConfirmDialog
     {
-        public ConfirmExitDialog(Action confirm, Action cancel)
+        public ConfirmExitDialog(Action confirm, Action onCancel = null)
+            : base("exit osu!", confirm, onCancel)
         {
-            HeaderText = "Are you sure you want to exit?";
-            BodyText = "Last chance to back out.";
-
-            Icon = FontAwesome.Solid.ExclamationTriangle;
-
-            Buttons = new PopupDialogButton[]
-            {
-                new PopupDialogOkButton
-                {
-                    Text = @"Goodbye",
-                    Action = confirm
-                },
-                new PopupDialogCancelButton
-                {
-                    Text = @"Just a little more",
-                    Action = cancel
-                },
-            };
+            ButtonConfirm.Text = "Let me out!";
+            ButtonCancel.Text = "Just a little more...";
         }
     }
 }

From d332fd2414c131a363437e1c71fb27047eaa48c1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:53:47 +0900
Subject: [PATCH 137/434] Handle case where local user tries to change beatmap
 while not the host

---
 .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs   | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 06d83e495c..e09e1fc3d4 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -402,6 +402,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
             if (!this.IsCurrentScreen())
                 return;
 
+            if (!client.IsHost)
+            {
+                // todo: should handle this when the request queue is implemented.
+                // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap
+                // flow may need to change to support an "unable to present" return value.
+                return;
+            }
+
             this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset));
         }
     }

From cb4c3503a01da92d0a68222702fa3b958da05fe1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 14:50:54 +0900
Subject: [PATCH 138/434] Confirm exiting a multiplayer match

---
 .../Multiplayer/MultiplayerMatchSubScreen.cs  | 26 ++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 4fbea4e3be..51445b0668 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -15,6 +15,8 @@ using osu.Framework.Threading;
 using osu.Game.Configuration;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Dialog;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.OnlinePlay.Components;
 using osu.Game.Screens.OnlinePlay.Match;
@@ -279,14 +281,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
             Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
         }
 
+        [Resolved]
+        private DialogOverlay dialogOverlay { get; set; }
+
+        private bool exitConfirmed;
+
         public override bool OnBackButton()
         {
-            if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
+            if (client.Room == null)
+            {
+                // room has not been created yet; exit immediately.
+                return base.OnBackButton();
+            }
+
+            if (settingsOverlay.State.Value == Visibility.Visible)
             {
                 settingsOverlay.Hide();
                 return true;
             }
 
+            if (!exitConfirmed)
+            {
+                dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () =>
+                {
+                    exitConfirmed = true;
+                    this.Exit();
+                }));
+
+                return true;
+            }
+
             return base.OnBackButton();
         }
 

From 0ede28da2f0f79b11cf2355cda4264f4d686ad12 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 15:24:55 +0900
Subject: [PATCH 139/434] Fix test failures due to missing dependency

---
 .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index 51445b0668..f1d8bf97fd 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -281,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
             Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
         }
 
-        [Resolved]
+        [Resolved(canBeNull: true)]
         private DialogOverlay dialogOverlay { get; set; }
 
         private bool exitConfirmed;
@@ -300,7 +300,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
                 return true;
             }
 
-            if (!exitConfirmed)
+            if (!exitConfirmed && dialogOverlay != null)
             {
                 dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () =>
                 {

From 002646370ccd589f94bf1d6947982961393003e6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 16:47:42 +0900
Subject: [PATCH 140/434] Move bindable logic in MouseSettings to LoadComplete

---
 .../Settings/Sections/Input/MouseSettings.cs  | 32 ++++++++++++-------
 1 file changed, 21 insertions(+), 11 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 768a18cca0..7599a748ab 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -17,7 +17,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         protected override string Header => "Mouse";
 
         private readonly BindableBool rawInputToggle = new BindableBool();
-        private Bindable<double> sensitivityBindable = new BindableDouble();
+
+        private Bindable<double> configSensitivity;
+
+        private Bindable<double> localSensitivity = new BindableDouble();
+
         private Bindable<string> ignoredInputHandlers;
 
         private Bindable<WindowMode> windowMode;
@@ -26,12 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         [BackgroundDependencyLoader]
         private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
         {
-            var configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity);
-
             // use local bindable to avoid changing enabled state of game host's bindable.
-            sensitivityBindable = configSensitivity.GetUnboundCopy();
-            configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue);
-            sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue);
+            configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity);
+            localSensitivity = configSensitivity.GetUnboundCopy();
+
+            windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
+            ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
 
             Children = new Drawable[]
             {
@@ -43,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 new SensitivitySetting
                 {
                     LabelText = "Cursor sensitivity",
-                    Current = sensitivityBindable
+                    Current = localSensitivity
                 },
                 new SettingsCheckbox
                 {
@@ -66,8 +70,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons)
                 },
             };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            configSensitivity.BindValueChanged(val => localSensitivity.Value = val.NewValue, true);
+            localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue);
 
-            windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
             windowMode.BindValueChanged(mode =>
             {
                 var isFullscreen = mode.NewValue == WindowMode.Fullscreen;
@@ -87,7 +98,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
             {
                 rawInputToggle.Disabled = true;
-                sensitivityBindable.Disabled = true;
+                localSensitivity.Disabled = true;
             }
             else
             {
@@ -100,12 +111,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler;
                 };
 
-                ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
                 ignoredInputHandlers.ValueChanged += handler =>
                 {
                     bool raw = !handler.NewValue.Contains("Raw");
                     rawInputToggle.Value = raw;
-                    sensitivityBindable.Disabled = !raw;
+                    localSensitivity.Disabled = !raw;
                 };
 
                 ignoredInputHandlers.TriggerChange();

From 012b48dbe51e972b291f37f88dfe0788cd9adb84 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 3 Mar 2021 19:03:44 +0900
Subject: [PATCH 141/434] Remove explicit public definition

Interface members are public by default.
---
 osu.Game/Screens/IHandlePresentBeatmap.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs
index b94df630ef..60801fb3eb 100644
--- a/osu.Game/Screens/IHandlePresentBeatmap.cs
+++ b/osu.Game/Screens/IHandlePresentBeatmap.cs
@@ -18,6 +18,6 @@ namespace osu.Game.Screens
         /// </summary>
         /// <param name="beatmap">The beatmap to be selected.</param>
         /// <param name="ruleset">The ruleset to be selected.</param>
-        public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset);
+        void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset);
     }
 }

From 6affe33fb275acb9d3feee55d08433f88fb1e25a Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 3 Mar 2021 19:40:19 +0900
Subject: [PATCH 142/434] Fix another test scene

---
 .../TestSceneMultiplayerRoomManager.cs        | 46 +++++++++++++------
 .../Multiplayer/TestMultiplayerClient.cs      | 14 ++++--
 .../TestMultiplayerRoomContainer.cs           | 10 ++--
 .../Multiplayer/TestMultiplayerRoomManager.cs |  8 ++--
 4 files changed, 52 insertions(+), 26 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
index 6de5704410..91c15de69f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs
@@ -1,10 +1,13 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using NUnit.Framework;
 using osu.Framework.Graphics;
 using osu.Framework.Testing;
 using osu.Game.Online.Rooms;
+using osu.Game.Screens.OnlinePlay.Components;
+using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual.Multiplayer
 {
@@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room { Name = { Value = "1" } });
+                    roomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
                     roomManager.PartRoom();
-                    roomManager.CreateRoom(new Room { Name = { Value = "2" } });
+                    roomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
                     roomManager.PartRoom();
                     roomManager.ClearRooms();
                 });
             });
 
-            AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
+            AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
             AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
         }
 
@@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.PartRoom();
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.PartRoom();
                 });
             });
 
             AddStep("disconnect", () => roomContainer.Client.Disconnect());
 
-            AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
+            AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0);
             AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
         }
 
@@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.PartRoom();
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.PartRoom();
                 });
             });
@@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("disconnect", () => roomContainer.Client.Disconnect());
             AddStep("connect", () => roomContainer.Client.Connect());
 
-            AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
+            AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
             AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
         }
 
@@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.ClearRooms();
                 });
             });
 
-            AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
+            AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0);
             AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
         }
 
@@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                 });
             });
 
@@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    roomManager.CreateRoom(new Room());
+                    roomManager.CreateRoom(createRoom());
                     roomManager.PartRoom();
                 });
             });
@@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             {
                 createRoomManager().With(d => d.OnLoadComplete += _ =>
                 {
-                    var r = new Room();
+                    var r = createRoom();
                     roomManager.CreateRoom(r);
                     roomManager.PartRoom();
                     roomManager.JoinRoom(r);
@@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
         }
 
+        private Room createRoom(Action<Room> initFunc = null)
+        {
+            var room = new Room();
+
+            room.Name.Value = "test room";
+            room.Playlist.Add(new PlaylistItem
+            {
+                Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
+                Ruleset = { Value = Ruleset.Value }
+            });
+
+            initFunc?.Invoke(room);
+            return room;
+        }
+
         private TestMultiplayerRoomManager createRoomManager()
         {
             Child = roomContainer = new TestMultiplayerRoomContainer
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index 6a901fc45b..c03364a391 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -28,12 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
         [Resolved]
         private IAPIProvider api { get; set; } = null!;
 
-        [Resolved]
-        private Room apiRoom { get; set; } = null!;
-
         [Resolved]
         private BeatmapManager beatmaps { get; set; } = null!;
 
+        private readonly TestMultiplayerRoomManager roomManager;
+
+        public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
+        {
+            this.roomManager = roomManager;
+        }
+
         public void Connect() => isConnected.Value = true;
 
         public void Disconnect() => isConnected.Value = false;
@@ -98,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
         protected override Task<MultiplayerRoom> JoinRoom(long roomId)
         {
-            Debug.Assert(apiRoom != null);
+            var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId);
 
             var user = new MultiplayerRoomUser(api.LocalUser.Value.Id)
             {
@@ -178,8 +182,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
         protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
         {
             Debug.Assert(Room != null);
-            Debug.Assert(apiRoom != null);
 
+            var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID);
             var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
                       ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet;
 
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
index 860caef071..e57411d04d 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs
@@ -32,11 +32,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             RelativeSizeAxes = Axes.Both;
 
+            RoomManager = new TestMultiplayerRoomManager();
+            Client = new TestMultiplayerClient(RoomManager);
+            OngoingOperationTracker = new OngoingOperationTracker();
+
             AddRangeInternal(new Drawable[]
             {
-                Client = new TestMultiplayerClient(),
-                RoomManager = new TestMultiplayerRoomManager(),
-                OngoingOperationTracker = new OngoingOperationTracker(),
+                Client,
+                RoomManager,
+                OngoingOperationTracker,
                 content = new Container { RelativeSizeAxes = Axes.Both }
             });
         }
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index 022c297ccd..7e824c4d7c 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         [Cached]
         public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
 
-        private readonly List<Room> rooms = new List<Room>();
+        public new readonly List<Room> Rooms = new List<Room>();
 
         protected override void LoadComplete()
         {
@@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                         for (int i = 0; i < createdRoom.Playlist.Count; i++)
                             createdRoom.Playlist[i].ID = currentPlaylistItemId++;
 
-                        rooms.Add(createdRoom);
+                        Rooms.Add(createdRoom);
                         createRoomRequest.TriggerSuccess(createdRoom);
                         break;
 
@@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                     case GetRoomsRequest getRoomsRequest:
                         var roomsWithoutParticipants = new List<Room>();
 
-                        foreach (var r in rooms)
+                        foreach (var r in Rooms)
                         {
                             var newRoom = new Room();
 
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                         break;
 
                     case GetRoomRequest getRoomRequest:
-                        getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
+                        getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
                         break;
 
                     case GetBeatmapSetRequest getBeatmapSetRequest:

From 0f5bce70ad4eec310f54113341e747e552b2a4e4 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 20:34:36 +0900
Subject: [PATCH 143/434] Split confirmation dialog classes apart

---
 osu.Game/Overlays/Dialog/ConfirmDialog.cs     | 17 +++++-----
 osu.Game/Screens/Menu/ConfirmExitDialog.cs    | 31 ++++++++++++++++---
 .../Multiplayer/MultiplayerMatchSubScreen.cs  |  2 +-
 3 files changed, 34 insertions(+), 16 deletions(-)

diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
index 6f160daf97..a87c06ffdf 100644
--- a/osu.Game/Overlays/Dialog/ConfirmDialog.cs
+++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
@@ -11,30 +11,27 @@ namespace osu.Game.Overlays.Dialog
     /// </summary>
     public class ConfirmDialog : PopupDialog
     {
-        protected PopupDialogOkButton ButtonConfirm;
-        protected PopupDialogCancelButton ButtonCancel;
-
         /// <summary>
-        /// Construct a new dialog.
+        /// Construct a new confirmation dialog.
         /// </summary>
-        /// <param name="description">The description of the action to be displayed to the user.</param>
+        /// <param name="message">The description of the action to be displayed to the user.</param>
         /// <param name="onConfirm">An action to perform on confirmation.</param>
         /// <param name="onCancel">An optional action to perform on cancel.</param>
-        public ConfirmDialog(string description, Action onConfirm, Action onCancel = null)
+        public ConfirmDialog(string message, Action onConfirm, Action onCancel = null)
         {
-            HeaderText = $"Are you sure you want to {description}?";
-            BodyText = "Last chance to back out.";
+            HeaderText = message;
+            BodyText = "Last chance to turn back";
 
             Icon = FontAwesome.Solid.ExclamationTriangle;
 
             Buttons = new PopupDialogButton[]
             {
-                ButtonConfirm = new PopupDialogOkButton
+                new PopupDialogOkButton
                 {
                     Text = @"Yes",
                     Action = onConfirm
                 },
-                ButtonCancel = new PopupDialogCancelButton
+                new PopupDialogCancelButton
                 {
                     Text = @"Cancel",
                     Action = onCancel
diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
index 41cc7b480c..6488a2fd63 100644
--- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs
+++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs
@@ -2,17 +2,38 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using osu.Framework.Graphics.Sprites;
 using osu.Game.Overlays.Dialog;
 
 namespace osu.Game.Screens.Menu
 {
-    public class ConfirmExitDialog : ConfirmDialog
+    public class ConfirmExitDialog : PopupDialog
     {
-        public ConfirmExitDialog(Action confirm, Action onCancel = null)
-            : base("exit osu!", confirm, onCancel)
+        /// <summary>
+        /// Construct a new exit confirmation dialog.
+        /// </summary>
+        /// <param name="onConfirm">An action to perform on confirmation.</param>
+        /// <param name="onCancel">An optional action to perform on cancel.</param>
+        public ConfirmExitDialog(Action onConfirm, Action onCancel = null)
         {
-            ButtonConfirm.Text = "Let me out!";
-            ButtonCancel.Text = "Just a little more...";
+            HeaderText = "Are you sure you want to exit osu!?";
+            BodyText = "Last chance to turn back";
+
+            Icon = FontAwesome.Solid.ExclamationTriangle;
+
+            Buttons = new PopupDialogButton[]
+            {
+                new PopupDialogOkButton
+                {
+                    Text = @"Let me out!",
+                    Action = onConfirm
+                },
+                new PopupDialogCancelButton
+                {
+                    Text = @"Just a little more...",
+                    Action = onCancel
+                },
+            };
         }
     }
 }
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index f1d8bf97fd..5a9a26d997 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -302,7 +302,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
             if (!exitConfirmed && dialogOverlay != null)
             {
-                dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () =>
+                dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
                 {
                     exitConfirmed = true;
                     this.Exit();

From 534e16237a5e74dd448af1d37e72d580393e5ba3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 3 Mar 2021 20:36:41 +0900
Subject: [PATCH 144/434] Remove unnecessary intial construction of bindable

---
 osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 7599a748ab..c3deb385cd 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private Bindable<double> configSensitivity;
 
-        private Bindable<double> localSensitivity = new BindableDouble();
+        private Bindable<double> localSensitivity;
 
         private Bindable<string> ignoredInputHandlers;
 

From 1ecb1d122a55500204eaea01d2321a1a9c71c707 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 3 Mar 2021 21:54:34 +0900
Subject: [PATCH 145/434] Fix up TestSceneMultiplayer

---
 .../Multiplayer/TestSceneMultiplayer.cs       | 36 ++++++-------------
 1 file changed, 11 insertions(+), 25 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 2e39471dc0..bb5db5b803 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -1,13 +1,13 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Online.Multiplayer;
 using osu.Game.Screens.OnlinePlay.Components;
-using osu.Game.Users;
 
 namespace osu.Game.Tests.Visual.Multiplayer
 {
-    public class TestSceneMultiplayer : MultiplayerTestScene
+    public class TestSceneMultiplayer : ScreenTestScene
     {
         public TestSceneMultiplayer()
         {
@@ -17,30 +17,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddUntilStep("wait for loaded", () => multi.IsLoaded);
         }
 
-        [Test]
-        public void TestOneUserJoinedMultipleTimes()
-        {
-            var user = new User { Id = 33 };
-
-            AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
-
-            AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
-        }
-
-        [Test]
-        public void TestOneUserLeftMultipleTimes()
-        {
-            var user = new User { Id = 44 };
-
-            AddStep("add user", () => Client.AddUser(user));
-            AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
-
-            AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
-            AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
-        }
-
         private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
         {
+            [Cached(typeof(StatefulMultiplayerClient))]
+            public readonly TestMultiplayerClient Client;
+
+            public TestMultiplayer()
+            {
+                AddInternal(Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager));
+            }
+
             protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
         }
     }

From 0f83b66cdabb1aad42d7f9d1c205b38089d7b2c2 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 3 Mar 2021 22:01:03 +0900
Subject: [PATCH 146/434] Add separate test for stateful multiplayer client

---
 .../StatefulMultiplayerClientTest.cs          | 35 +++++++++++++++++++
 1 file changed, 35 insertions(+)
 create mode 100644 osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs

diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
new file mode 100644
index 0000000000..82ce588c6f
--- /dev/null
+++ b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Tests.Visual.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.OnlinePlay
+{
+    [HeadlessTest]
+    public class StatefulMultiplayerClientTest : MultiplayerTestScene
+    {
+        [Test]
+        public void TestUserAddedOnJoin()
+        {
+            var user = new User { Id = 33 };
+
+            AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
+            AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+        }
+
+        [Test]
+        public void TestUserRemovedOnLeave()
+        {
+            var user = new User { Id = 44 };
+
+            AddStep("add user", () => Client.AddUser(user));
+            AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
+
+            AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
+            AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
+        }
+    }
+}

From 77607c06eba37de48cb0670a4e7e09d9a9c8e4ae Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 3 Mar 2021 22:07:39 +0900
Subject: [PATCH 147/434] Fix not being able to enter gameplay in
 TestSceneMultiplayer

---
 .../Visual/Multiplayer/TestSceneMultiplayer.cs  | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index bb5db5b803..78bc51e47b 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -9,12 +9,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
 {
     public class TestSceneMultiplayer : ScreenTestScene
     {
+        private TestMultiplayer multiplayerScreen;
+
         public TestSceneMultiplayer()
         {
-            var multi = new TestMultiplayer();
+            AddStep("show", () =>
+            {
+                multiplayerScreen = new TestMultiplayer();
 
-            AddStep("show", () => LoadScreen(multi));
-            AddUntilStep("wait for loaded", () => multi.IsLoaded);
+                // Needs to be added at a higher level since the multiplayer screen becomes non-current.
+                Child = multiplayerScreen.Client;
+
+                LoadScreen(multiplayerScreen);
+            });
+
+            AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded);
         }
 
         private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
@@ -24,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
             public TestMultiplayer()
             {
-                AddInternal(Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager));
+                Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
             }
 
             protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();

From f9148eec206b1126ed0af01f24610d2eca2ab00d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 3 Mar 2021 21:33:41 +0100
Subject: [PATCH 148/434] Refactor filter query parsing helper methods

In preparation for exposition as public.
---
 .../Filtering/FilterQueryParserTest.cs        | 11 +++
 osu.Game/Screens/Select/FilterQueryParser.cs  | 75 ++++++++++++-------
 2 files changed, 59 insertions(+), 27 deletions(-)

diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index d835e58b29..49389e67aa 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -215,6 +215,17 @@ namespace osu.Game.Tests.NonVisual.Filtering
             Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText);
         }
 
+        [TestCase("cs=nope")]
+        [TestCase("bpm>=bad")]
+        [TestCase("divisor<nah")]
+        [TestCase("status=noidea")]
+        public void TestInvalidKeywordValueIsIgnored(string query)
+        {
+            var filterCriteria = new FilterCriteria();
+            FilterQueryParser.ApplyQueries(filterCriteria, query);
+            Assert.AreEqual(query, filterCriteria.SearchText);
+        }
+
         [Test]
         public void TestCustomKeywordIsParsed()
         {
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index c81a72d938..ce937d07b1 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -34,37 +34,37 @@ namespace osu.Game.Screens.Select
         {
             switch (key)
             {
-                case "stars" when parseFloatWithPoint(value, out var stars):
-                    return updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
+                case "stars":
+                    return tryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
 
-                case "ar" when parseFloatWithPoint(value, out var ar):
-                    return updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
+                case "ar":
+                    return tryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
 
-                case "dr" when parseFloatWithPoint(value, out var dr):
-                case "hp" when parseFloatWithPoint(value, out dr):
-                    return updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
+                case "dr":
+                case "hp":
+                    return tryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
 
-                case "cs" when parseFloatWithPoint(value, out var cs):
-                    return updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
+                case "cs":
+                    return tryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
 
-                case "bpm" when parseDoubleWithPoint(value, out var bpm):
-                    return updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
+                case "bpm":
+                    return tryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
 
-                case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
-                    var scale = getLengthScale(value);
-                    return updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
+                case "length":
+                    return tryUpdateLengthRange(criteria, op, value);
 
-                case "divisor" when parseInt(value, out var divisor):
-                    return updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
+                case "divisor":
+                    return tryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
 
-                case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
-                    return updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
+                case "status":
+                    return tryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
+                        (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
 
                 case "creator":
-                    return updateCriteriaText(ref criteria.Creator, op, value);
+                    return tryUpdateCriteriaText(ref criteria.Creator, op, value);
 
                 case "artist":
-                    return updateCriteriaText(ref criteria.Artist, op, value);
+                    return tryUpdateCriteriaText(ref criteria.Artist, op, value);
 
                 default:
                     return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
@@ -104,16 +104,16 @@ namespace osu.Game.Screens.Select
             value.EndsWith('m') ? 60000 :
             value.EndsWith('h') ? 3600000 : 1000;
 
-        private static bool parseFloatWithPoint(string value, out float result) =>
+        private static bool tryParseFloatWithPoint(string value, out float result) =>
             float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
 
-        private static bool parseDoubleWithPoint(string value, out double result) =>
+        private static bool tryParseDoubleWithPoint(string value, out double result) =>
             double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
 
-        private static bool parseInt(string value, out int result) =>
+        private static bool tryParseInt(string value, out int result) =>
             int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
 
-        private static bool updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
+        private static bool tryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
         {
             switch (op)
             {
@@ -126,7 +126,10 @@ namespace osu.Game.Screens.Select
             }
         }
 
-        private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
+        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
+            => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
+
+        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
         {
             switch (op)
             {
@@ -158,7 +161,10 @@ namespace osu.Game.Screens.Select
             return true;
         }
 
-        private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
+        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
+            => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
+
+        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
         {
             switch (op)
             {
@@ -190,7 +196,13 @@ namespace osu.Game.Screens.Select
             return true;
         }
 
-        private static bool updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
+        private delegate bool TryParseFunction<T>(string val, out T value);
+
+        private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string value, TryParseFunction<T> conversionFunc)
+            where T : struct
+            => conversionFunc.Invoke(value, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
+
+        private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
             where T : struct
         {
             switch (op)
@@ -227,5 +239,14 @@ namespace osu.Game.Screens.Select
 
             return true;
         }
+
+        private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
+        {
+            if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length))
+                return false;
+
+            var scale = getLengthScale(val);
+            return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
+        }
     }
 }

From f733d1ec1fcf08a147d24aeab20d3e8187936c71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 3 Mar 2021 21:58:34 +0100
Subject: [PATCH 149/434] Expose and document query parser and helpers

---
 .../Rulesets/Filter/IRulesetFilterCriteria.cs | 11 +++
 osu.Game/Screens/Select/FilterQueryParser.cs  | 89 +++++++++++++++----
 2 files changed, 84 insertions(+), 16 deletions(-)

diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
index a83f87d72b..13cc41f8e0 100644
--- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
+++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
@@ -31,6 +31,17 @@ namespace osu.Game.Rulesets.Filter
         /// {key}{op}{value}
         /// </code>
         /// </summary>
+        /// <remarks>
+        /// <para>
+        /// For adding optional string criteria, <see cref="FilterCriteria.OptionalTextFilter"/> can be used for matching,
+        /// along with <see cref="FilterQueryParser.TryUpdateCriteriaText"/> for parsing.
+        /// </para>
+        /// <para>
+        /// For adding numerical-type range criteria, <see cref="FilterCriteria.OptionalRange{T}"/> can be used for matching,
+        /// along with <see cref="FilterQueryParser.TryUpdateCriteriaRange{T}(ref osu.Game.Screens.Select.FilterCriteria.OptionalRange{T},osu.Game.Screens.Select.Filter.Operator,string,FilterQueryParser.TryParseFunction{T})"/>
+        /// and <see cref="float"/>- and <see cref="double"/>-typed overloads for parsing.
+        /// </para>
+        /// </remarks>
         /// <param name="key">The key (name) of the criterion.</param>
         /// <param name="op">The operator in the criterion.</param>
         /// <param name="value">The value of the criterion.</param>
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index ce937d07b1..ea7f233bea 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -9,7 +9,10 @@ using osu.Game.Screens.Select.Filter;
 
 namespace osu.Game.Screens.Select
 {
-    internal static class FilterQueryParser
+    /// <summary>
+    /// Utility class used for parsing song select filter queries entered via the search box.
+    /// </summary>
+    public static class FilterQueryParser
     {
         private static readonly Regex query_syntax_regex = new Regex(
             @"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
@@ -35,36 +38,36 @@ namespace osu.Game.Screens.Select
             switch (key)
             {
                 case "stars":
-                    return tryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
+                    return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
 
                 case "ar":
-                    return tryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
+                    return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
 
                 case "dr":
                 case "hp":
-                    return tryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
+                    return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
 
                 case "cs":
-                    return tryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
+                    return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
 
                 case "bpm":
-                    return tryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
+                    return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
 
                 case "length":
                     return tryUpdateLengthRange(criteria, op, value);
 
                 case "divisor":
-                    return tryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
+                    return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
 
                 case "status":
-                    return tryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
+                    return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
                         (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
 
                 case "creator":
-                    return tryUpdateCriteriaText(ref criteria.Creator, op, value);
+                    return TryUpdateCriteriaText(ref criteria.Creator, op, value);
 
                 case "artist":
-                    return tryUpdateCriteriaText(ref criteria.Artist, op, value);
+                    return TryUpdateCriteriaText(ref criteria.Artist, op, value);
 
                 default:
                     return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
@@ -113,7 +116,18 @@ namespace osu.Game.Screens.Select
         private static bool tryParseInt(string value, out int result) =>
             int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
 
-        private static bool tryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
+        /// <summary>
+        /// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>.
+        /// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into
+        /// <paramref name="textFilter"/>.
+        /// </summary>
+        /// <param name="textFilter">The <see cref="FilterCriteria.OptionalTextFilter"/> to store the parsed data into, if successful.</param>
+        /// <param name="op">
+        /// The operator for the keyword filter.
+        /// Only <see cref="Operator.Equal"/> is valid for textual filters.
+        /// </param>
+        /// <param name="value">The value of the keyword filter.</param>
+        public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
         {
             switch (op)
             {
@@ -126,7 +140,20 @@ namespace osu.Game.Screens.Select
             }
         }
 
-        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
+        /// <summary>
+        /// Attempts to parse a keyword filter of type <see cref="float"/>
+        /// from the specified <paramref name="op"/> and <paramref name="val"/>.
+        /// If <paramref name="val"/> can be parsed as a <see cref="float"/>, the function returns <c>true</c>
+        /// and the resulting range constraint is stored into <paramref name="range"/>.
+        /// </summary>
+        /// <param name="range">
+        /// The <see cref="float"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
+        /// to store the parsed data into, if successful.
+        /// </param>
+        /// <param name="op">The operator for the keyword filter.</param>
+        /// <param name="val">The value of the keyword filter.</param>
+        /// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
+        public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
             => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
 
         private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
@@ -161,7 +188,20 @@ namespace osu.Game.Screens.Select
             return true;
         }
 
-        private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
+        /// <summary>
+        /// Attempts to parse a keyword filter of type <see cref="double"/>
+        /// from the specified <paramref name="op"/> and <paramref name="val"/>.
+        /// If <paramref name="val"/> can be parsed as a <see cref="double"/>, the function returns <c>true</c>
+        /// and the resulting range constraint is stored into <paramref name="range"/>.
+        /// </summary>
+        /// <param name="range">
+        /// The <see cref="double"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
+        /// to store the parsed data into, if successful.
+        /// </param>
+        /// <param name="op">The operator for the keyword filter.</param>
+        /// <param name="val">The value of the keyword filter.</param>
+        /// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
+        public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
             => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
 
         private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
@@ -196,11 +236,28 @@ namespace osu.Game.Screens.Select
             return true;
         }
 
-        private delegate bool TryParseFunction<T>(string val, out T value);
+        /// <summary>
+        /// Used to determine whether the string value <paramref name="val"/> can be converted to type <typeparamref name="T"/>.
+        /// If conversion can be performed, the delegate returns <c>true</c>
+        /// and the conversion result is returned in the <c>out</c> parameter <paramref name="parsed"/>.
+        /// </summary>
+        /// <param name="val">The string value to attempt parsing for.</param>
+        /// <param name="parsed">The parsed value, if conversion is possible.</param>
+        public delegate bool TryParseFunction<T>(string val, out T parsed);
 
-        private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string value, TryParseFunction<T> conversionFunc)
+        /// <summary>
+        /// Attempts to parse a keyword filter of type <typeparamref name="T"/>,
+        /// from the specified <paramref name="op"/> and <paramref name="val"/>.
+        /// If <paramref name="val"/> can be parsed into <typeparamref name="T"/> using <paramref name="parseFunction"/>, the function returns <c>true</c>
+        /// and the resulting range constraint is stored into <paramref name="range"/>.
+        /// </summary>
+        /// <param name="range">The <see cref="FilterCriteria.OptionalRange{T}"/> to store the parsed data into, if successful.</param>
+        /// <param name="op">The operator for the keyword filter.</param>
+        /// <param name="val">The value of the keyword filter.</param>
+        /// <param name="parseFunction">Function used to determine if <paramref name="val"/> can be converted to type <typeparamref name="T"/>.</param>
+        public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string val, TryParseFunction<T> parseFunction)
             where T : struct
-            => conversionFunc.Invoke(value, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
+            => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
 
         private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
             where T : struct

From fe64c3dbd4de6ada5be2ca5112c65c2f17abb607 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Thu, 4 Mar 2021 14:59:08 +0300
Subject: [PATCH 150/434] Refrain from disabling cursor sensitivity at
 config-level

---
 osu.Game/OsuGame.cs | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 7db85d0d66..203cc458e0 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -880,13 +880,8 @@ namespace osu.Game
             switch (action)
             {
                 case GlobalAction.ResetInputSettings:
-                    var sensitivity = frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity);
-
-                    sensitivity.Disabled = false;
-                    sensitivity.Value = 1;
-                    sensitivity.Disabled = true;
-
-                    frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty);
+                    frameworkConfig.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers).SetDefault();
+                    frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity).SetDefault();
                     frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
                     return true;
 

From 132fcda08987f880767bf20d2eb4a785f09dd9dd Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Thu, 4 Mar 2021 15:00:46 +0300
Subject: [PATCH 151/434] Force config sensitivity value to local setting
 bindable

Re-enable the local bindable to update the sensitivity value then change back to whatever state it was in previously.
---
 .../Overlays/Settings/Sections/Input/MouseSettings.cs  | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index c3deb385cd..3a78cff890 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -76,7 +76,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             base.LoadComplete();
 
-            configSensitivity.BindValueChanged(val => localSensitivity.Value = val.NewValue, true);
+            configSensitivity.BindValueChanged(val =>
+            {
+                var disabled = localSensitivity.Disabled;
+
+                localSensitivity.Disabled = false;
+                localSensitivity.Value = val.NewValue;
+                localSensitivity.Disabled = disabled;
+            }, true);
+
             localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue);
 
             windowMode.BindValueChanged(mode =>

From 12b7d9e06d16b52a600c5506913c5569ee12b2ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 6 Mar 2021 12:16:01 +0100
Subject: [PATCH 152/434] Simplify custom filter criteria retrieval

---
 osu.Game/Screens/Select/FilterControl.cs | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index 983928ac51..298b6e49bd 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -35,9 +35,6 @@ namespace osu.Game.Screens.Select
 
         private Bindable<GroupMode> groupMode;
 
-        [Resolved]
-        private RulesetStore rulesets { get; set; }
-
         public FilterCriteria CreateCriteria()
         {
             Debug.Assert(ruleset.Value.ID != null);
@@ -59,7 +56,7 @@ namespace osu.Game.Screens.Select
             if (!maximumStars.IsDefault)
                 criteria.UserStarDifficulty.Max = maximumStars.Value;
 
-            criteria.RulesetCriteria = rulesets.GetRuleset(ruleset.Value.ID.Value).CreateInstance().CreateRulesetFilterCriteria();
+            criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
 
             FilterQueryParser.ApplyQueries(criteria, query);
             return criteria;

From 06e42b4b4c2bab783e277fcbc86ef5f26efc50aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 6 Mar 2021 16:02:20 +0100
Subject: [PATCH 153/434] Fix taiko leaving behind empty judgements on legacy
 skins

---
 .../Skinning/Legacy/TaikoLegacySkinTransformer.cs  |  2 +-
 osu.Game/Rulesets/Judgements/DrawableJudgement.cs  | 14 +++++---------
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 9f29675230..40dc149ec9 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
             {
                 // if a taiko skin is providing explosion sprites, hide the judgements completely
                 if (hasExplosion.Value)
-                    return Drawable.Empty();
+                    return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
             }
 
             if (!(component is TaikoSkinComponent taikoComponent))
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index da9bb8a09d..feeafb7151 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -150,17 +150,13 @@ namespace osu.Game.Rulesets.Judgements
                 }
 
                 if (JudgementBody.Drawable is IAnimatableJudgement animatable)
-                {
-                    var drawableAnimation = (Drawable)animatable;
-
                     animatable.PlayAnimation();
 
-                    // a derived version of DrawableJudgement may be proposing a lifetime.
-                    // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
-                    double lastTransformTime = drawableAnimation.LatestTransformEndTime;
-                    if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
-                        LifetimeEnd = lastTransformTime;
-                }
+                // a derived version of DrawableJudgement may be proposing a lifetime.
+                // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
+                double lastTransformTime = JudgementBody.Drawable.LatestTransformEndTime;
+                if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
+                    LifetimeEnd = lastTransformTime;
             }
         }
 

From 1525480e73196e9cbbef3128b012e04e130c84b6 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sat, 6 Mar 2021 19:18:40 +0300
Subject: [PATCH 154/434] Demonstrate value of `SPINNER_TOP_OFFSET` to being
 more sensible

---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 1f1fd1fbd9..5df8f8a485 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             /// for positioning some legacy spinner components perfectly as in stable.
             /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
             /// </summary>
-            public const float SPINNER_TOP_OFFSET = 29f;
+            public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
 
             public LegacyCoordinatesContainer()
             {

From 8f4dadb06a393f760565ee61c3066644975b8c60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 6 Mar 2021 15:06:16 +0100
Subject: [PATCH 155/434] Enable pooling for taiko judgements

---
 .../UI/DrawableTaikoJudgement.cs              | 11 --------
 osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs  | 25 ++++++++++++++-----
 2 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index b5e35f88b5..1ad1e4495c 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -3,7 +3,6 @@
 
 using osu.Framework.Graphics;
 using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects.Drawables;
 
 namespace osu.Game.Rulesets.Taiko.UI
 {
@@ -12,16 +11,6 @@ namespace osu.Game.Rulesets.Taiko.UI
     /// </summary>
     public class DrawableTaikoJudgement : DrawableJudgement
     {
-        /// <summary>
-        /// Creates a new judgement text.
-        /// </summary>
-        /// <param name="judgedObject">The object which is being judged.</param>
-        /// <param name="result">The judgement to visualise.</param>
-        public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject)
-            : base(result, judgedObject)
-        {
-        }
-
         protected override void ApplyHitAnimations()
         {
             this.MoveToY(-100, 500);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 148ec7755e..d2e7b604bb 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -2,10 +2,12 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Objects.Drawables;
@@ -17,6 +19,7 @@ using osu.Game.Rulesets.UI.Scrolling;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.Judgements;
 using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Scoring;
 using osu.Game.Skinning;
 using osuTK;
 
@@ -38,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.UI
         internal Drawable HitTarget;
         private SkinnableDrawable mascot;
 
+        private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
+
         private ProxyContainer topLevelHitContainer;
         private Container rightArea;
         private Container leftArea;
@@ -159,6 +164,12 @@ namespace osu.Game.Rulesets.Taiko.UI
 
             RegisterPool<Swell, DrawableSwell>(5);
             RegisterPool<SwellTick, DrawableSwellTick>(100);
+
+            var hitWindows = new TaikoHitWindows();
+            foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
+                judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
+
+            AddRangeInternal(judgementPools.Values);
         }
 
         protected override void LoadComplete()
@@ -283,13 +294,15 @@ namespace osu.Game.Rulesets.Taiko.UI
                     break;
 
                 default:
-                    judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
+                    judgementContainer.Add(judgementPools[result.Type].Get(j =>
                     {
-                        Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft,
-                        Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre,
-                        RelativePositionAxes = Axes.X,
-                        X = result.IsHit ? judgedObject.Position.X : 0,
-                    });
+                        j.Apply(result, judgedObject);
+
+                        j.Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft;
+                        j.Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre;
+                        j.RelativePositionAxes = Axes.X;
+                        j.X = result.IsHit ? judgedObject.Position.X : 0;
+                    }));
 
                     var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
                     addExplosion(judgedObject, result.Type, type);

From 97f56340af36e85a13d85b02d477edee596f7f34 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 1 Mar 2021 20:57:25 +0300
Subject: [PATCH 156/434] Add legacy score font to testing old skin

---
 .../Resources/old-skin/score-0.png               | Bin 0 -> 3092 bytes
 .../Resources/old-skin/score-1.png               | Bin 0 -> 1237 bytes
 .../Resources/old-skin/score-2.png               | Bin 0 -> 3134 bytes
 .../Resources/old-skin/score-3.png               | Bin 0 -> 3712 bytes
 .../Resources/old-skin/score-4.png               | Bin 0 -> 2395 bytes
 .../Resources/old-skin/score-5.png               | Bin 0 -> 3067 bytes
 .../Resources/old-skin/score-6.png               | Bin 0 -> 3337 bytes
 .../Resources/old-skin/score-7.png               | Bin 0 -> 1910 bytes
 .../Resources/old-skin/score-8.png               | Bin 0 -> 3652 bytes
 .../Resources/old-skin/score-9.png               | Bin 0 -> 3561 bytes
 .../Resources/old-skin/score-comma.png           | Bin 0 -> 865 bytes
 .../Resources/old-skin/score-dot.png             | Bin 0 -> 771 bytes
 .../Resources/old-skin/score-percent.png         | Bin 0 -> 4904 bytes
 .../Resources/old-skin/score-x.png               | Bin 0 -> 2536 bytes
 .../Resources/old-skin/skin.ini                  |   6 +++++-
 15 files changed, 5 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png
 create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-0.png
new file mode 100644
index 0000000000000000000000000000000000000000..8304617d8c94a8400b50a90f364941bb02983065
GIT binary patch
literal 3092
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

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3b85eb873a9f46dcc300d92afda6155353bab39
GIT binary patch
literal 1237
zcmV;`1S<Q9P)<h;3K|Lk000e1NJLTq001HY002A)1^@s61Dj|m0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$cS%G+RCwC#Sx-wFK@^|eO?GXqjnxSK
zM@tc;77>wp5aXdYkHvliKZD@OPa^24A3!|xAgxEeM50JaMX=Hu|InJ&7&j)_-M*K+
zDPx*sXOc}TGy^|Y;_STN{N}wkGjBD|^Vm&jI=dk)RQZFZX^l)q6P~=G)UNPk_0<1^
z$ol$v&CcWF<DyZ6-VYN(g2E0UFQ5qHI!cNNMm51`hzz4aQ$U9Ri~8d1bASxsC*T5b
z3FCnvo~uNJX3h<xf!hK)g(t@yz%3$jMFfrkhcp?7;HoNO5CJq00rV1j0Qcy(yYws}
z7K=R`92~q2uykFwmzS5{t*x!ShyN7-HmM}PG^9qDW*P-N20R5k0XzcSpO~0<*5BXX
zJ1{WNH8eD2j*gBp%d(hhnk*Cwv0N_4Iy*aG1CdYA{|u0(rBd<{oz@>``VcTUGc)t%
zdAW6ujEorEXgD1HZCs2t{QWL8__GhtMdRtJL^OI42YN6yHT7zKetx2_udk}nY7N6Q
zpU?9#ELv)<5k<n}eBIO2(__(S6s49<EX87x?d<HhL?Yxz!Y{LyA}11wn1*2}WjNP$
z+4=doCnPrUDNV!^64GTNkw{%!TrkIRR5PwCgsj^rkpg-7>Ehzz>eABEX)2Y%S(ap%
zY2Zg_yeN=SCgKW7>G0&_WOHtA?(O8{WPEmZmhJ8Bp(FY(O%)<c)mdFy7F6Vf6FxXN
z&=(dKym&nRa&>i;_4M@A92A#vcX#))BBQD)0-F=}5t|Ybrs+MvM?i9ObMx4?ZC^#Q
z*=(K^;UtuohS5%mesW2f5%-O+fG>a)8j|o4M@5mEyDyL_c{`+hhjvi(nI8n1<}@2M
z)d+Eg`1!&&vyvjsElrJ(_QcbcpO0iRnVih_-{_gucV;|lwzjr@HWf8K1YB7~DoM~1
z2cifk;h-UjYltK3AB`&FY;SMN>^)uu0wcns$2mJYQy~)Q1xqPvT7A>=WRuh1x^jB|
zT9N<b65b}#CekL->ddONf3$*#cy09H7$`$V6YjiPD->}RCaKAgXyh1BItqB@CNs9d
z2~kZhuwy{!Jd#W%i}minx~?;!%BWW<eFmclT*jSxmn<yi#A30iP>l?X$hA`%U9kNW
z(Pc?AdP0*5VelbCAQGq|s^?InLXkk1hAAkb3uLrb_=s;p!>S_@(PVRpYSduN=D-;X
zbxRBoHDhCA9kTacRU$ZOsn~CtX0~2J!>9o=IjXM|g1m%#RF233zNgda6xPvdk-=nV
zSyqN>DK;tDSfQpyUF{rjw7<Wf*x1;3F+4mRp`3B89UUDNR#sL%Pz^3as?_(Z%2|Pd
zKRVHe0{k@64Y<$B9|@kZ@<#&9pk34z75heYw|&58Dtloq&IVgfvx@NAE0(ZM$zM>N
z16KZ&(n0;Mh)kLYeW!PP{X~igRvJN-A~~yAheZOWFb=O+=ZKI^eT!7BY}!Xkl}7v=
zM#iparj>iiwW<M!Uy-J&kF*cZZ6X!zmjD9*_`52{RF+Q&00000NkvXXu0mjf@8UPi

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-2.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f65eb7ca7e52a69d645d3550d932ec55c44acd7
GIT binary patch
literal 3134
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
Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-3.png
new file mode 100644
index 0000000000000000000000000000000000000000..82bec3babebd59263dc486e5900a3a01ea4aaed7
GIT binary patch
literal 3712
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

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-4.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e38c75a9ddf9dc183531b5332ecede96a3f0053
GIT binary patch
literal 2395
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
N002ovPDHLkV1f?Fo=yM&

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-5.png
new file mode 100644
index 0000000000000000000000000000000000000000..a562d9f2accc9592595b3b07669195d5b75406f8
GIT binary patch
literal 3067
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
JPDHLkV1kmK?iT<6

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-6.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4cf81f26e5cab5a068ce282ee22b15b92d0df12
GIT binary patch
literal 3337
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
TZ*tsI00000NkvXXu0mjfuoP_o

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-7.png
new file mode 100644
index 0000000000000000000000000000000000000000..a23f5379b223d61079e055162fdd93f107f0ec02
GIT binary patch
literal 1910
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

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-8.png
new file mode 100644
index 0000000000000000000000000000000000000000..430b18509d3b38dba358be5c825d62b5e3e96134
GIT binary patch
literal 3652
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
WlYXW<G5A^l0000<MNUMnLSTZTCi=1f

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-9.png
new file mode 100644
index 0000000000000000000000000000000000000000..add1202c31255cdf492a9f8bcdfeb21d105e8ef9
GIT binary patch
literal 3561
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

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-comma.png
new file mode 100644
index 0000000000000000000000000000000000000000..f68d32957ff8dc2e6e26e2b739eab85385548faf
GIT binary patch
literal 865
zcmV-n1D^beP)<h;3K|Lk000e1NJLTq000vJ002A)1^@s6gsOor0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU#14%?dRCwC#m`zUGKorNvj_nX=O4AUM
zHmRaYfI`Iq2=p4<pi;M-g41w_?t;_}0;O5eC=gZoE-g?8C${^4?we@hIILK7;gSB>
z8ION{U-N9C`MxinYv|$`M+}Q$F)W6~uoxD@Vpt6OzhDOaIXgR>o2*u=8V;)D@TF?C
z+T17S{{H?ym;%w+TADXmhT?$-O-NWvN0;UR_GXqT)+8VcT7_OfX+F1jnFIZS?x8!#
zh8}URz_+nygyLs1<TaFs3VgoGYX~%gdQgYY$=&gsWeiI~>rfHeIXpZ(*xcNFpGu`(
zwcG8nVHo$9mzUrA{eGSAow*&jOrP%XnP4jnsY0KQkB@6tS64o6^aq1MaO`wCezV#1
zPfkt<_8IyJm7o-H)Y71MrP!@^ySuwPg+f7WY;1^ZHX9uC`MfBVO5*6~=mXZgWiM7_
zg2J#BUQj3&i!V*nT&N{Y(}Zo?<X(njY0B~m!;+kU?B3oULS!t!T-OyB7Z)CTVXAeg
zEA^$8MLLm41XhH{bUGEI(a2W|m^HIZ))+l0SuU3|1dAF3Ycv{TE{8{&;;b&f$S(^)
zPCo_f^?DCnbB|mWb1+@8%tD$iUDubwP$Ivcf@#WrOm1&)FSTqknTUG5-ecG}%FQ{;
z*4EY{Eu%~hheL6T!TZu-*g^u-Zec{h$i?&XbDuZ5EfXNSMmPb1b(KoRigJ@+H#avF
zqX)$@H{5B}g7Pw%j93Ok@X0EffJm6nlXTIBi_>M9#738`=bqh+giU0>f>loV9|82+
zaU80#CVuw0IXW_vYCm)NDP(>JPj7d--Su=j9k?q&C<*&31~<mve9sJ~(L(G&lUsBS
zDnk|Q*MqICt({yhm%(+f)oNXzo}PZewFdMZ`T_l<TBDZ>y-*}fwjTm!p*P&Jvpnyz
z)m?^=o3|X6i423GymcH7s?X++lz!!LDcrGp4_y83T(4ux)LkG8*W#nj518@;Wz|pB
rcm*5<CX||!J0kK0tU^@SUjYUHNv2`hoe|eD00000NkvXXu0mjf`J|EL

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-dot.png
new file mode 100644
index 0000000000000000000000000000000000000000..80c39b874533f1996978af8ada97133b743870a8
GIT binary patch
literal 771
zcmV+e1N{7nP)<h;3K|Lk000e1NJLTq000sI002A)1^@s6YiQ~b0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!rAb6VRCwC#n7@w_K@`V#*;_mj;;F2}
zKtgfXoSGO53StEfv9_@@v7z!mkb+L+Pq3Ae%8G=71PPSHXtWRvM#STX5JZmKU3UDw
z&0DkHp_iDebSL>_XP0^3eKYf&7eE$5NN3S}(it^R$P@B}JRwiW6Y_*SAy4SP5!!0C
zTE9F<CX+HQGQY&>JKtC>>NlHB>x7U?fC3eMrUE}-6&6|p1Lzp5^Zgp4;-?Tka0Ir&
z1rTBaEJ%Zoz;`eQ3$UbTh&3rgX9|7}x(Kd<2wyLO^E@RI`3er0F^e8jrdymkWwHaJ
zU>EG6-At#`*8_n7WhgqG&Jb^YGairM;r=~O+gUez%_x%?%@8~chr`cux!mVouO}vx
ziI~l1Vmh6Q;czJ0?Y5Q8W=ZG?xDRe~_LPX7{ta*sJS`LoBN8x8Qyg!)u8YB7AXw}%
zxJ%jddP>*89q=fXO1<p&`x`~vVw4#&e~y?3oYfUt=yR;7WipxlP$(oRit=0Isj4bP
zqfr@SZ{hYb@3QK1ZF?vdiv?)lX^qBcnzrMzhvIX&*zTkK^;si_>TEF6vaC~s48xE{
zqtTROY?u4ELr$I4>-A5|<x=|7k3kZ_hFFzKrA;wxRD*<8U<L-|a=G4UG**knLfVuW
zXCf7gMaP&2>~39QXWMIpY}VCkbsmq$cd@;G3M5vml{BBvrEa%t<@5R1rBdky#=iw0
zIjg12+aCmBcDQpy_7jQ3UL+DBYmj&PwOZ{J`d!cgAHWx$%}VBs{OkvExyH8F_z-XN
zhAjBd4tYuwo)#^QA>$xAWkltmAR9EuCtu}5>6DRg%ppIcnq=Aa2%R;=oZ@xF169W7
zDr`CzH^QVBIE8JK`;M1drwJD)wp9Li|3LU5zyS0^RhH-3lmq|(002ovPDHLkV1mea
BUy%R+

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-percent.png
new file mode 100644
index 0000000000000000000000000000000000000000..fc750abc7e80287192efb65633e58b6b19e47125
GIT binary patch
literal 4904
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
aBftRVw!D&5)M2&&0000<MNUMnLSTaY-$WSz

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/score-x.png
new file mode 100644
index 0000000000000000000000000000000000000000..779773f8bdedd6c382b39e66b967b206cc5cdbda
GIT binary patch
literal 2536
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

literal 0
HcmV?d00001

diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
index 5369de24e9..89bcd68343 100644
--- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
+++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
@@ -1,2 +1,6 @@
 [General]
-Version: 1.0
\ No newline at end of file
+Version: 1.0
+
+[Fonts]
+HitCircleOverlap: 3
+ScoreOverlap: 3
\ No newline at end of file

From e4afe717d5b569fe9a6a7bca1bb8891b2cffee62 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 21:23:38 +0300
Subject: [PATCH 157/434] Publicize legacy coordinates container and sprite
 scale

---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 5df8f8a485..06443ca8b8 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
     public abstract class LegacySpinner : CompositeDrawable
     {
-        protected const float SPRITE_SCALE = 0.625f;
+        public const float SPRITE_SCALE = 0.625f;
 
         protected DrawableSpinner DrawableSpinner { get; private set; }
 
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         /// A <see cref="Container"/> simulating osu!stable's absolute screen-space,
         /// for perfect placements of legacy spinner components with legacy coordinates.
         /// </summary>
-        protected class LegacyCoordinatesContainer : Container
+        public class LegacyCoordinatesContainer : Container
         {
             /// <summary>
             /// An offset that simulates stable's spinner top offset,

From 1841a4d1c977e04377d7427681a93ad09d8c293d Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 21:37:25 +0300
Subject: [PATCH 158/434] Extract legacy spinner presence to lazy field

---
 .../Legacy/OsuLegacySkinTransformer.cs        | 30 ++++++++++++++++---
 1 file changed, 26 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index d74f885573..d4a403fbd2 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -13,6 +13,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
     {
         private Lazy<bool> hasHitCircle;
 
+        private Lazy<SpinnerStyle> spinnerStyle;
+
+        private bool hasSpinner => spinnerStyle.Value != SpinnerStyle.Modern;
+
         /// <summary>
         /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
         /// Their hittable area is 128px, but the actual circle portion is 118px.
@@ -30,6 +34,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         private void sourceChanged()
         {
             hasHitCircle = new Lazy<bool>(() => Source.GetTexture("hitcircle") != null);
+
+            spinnerStyle = new Lazy<SpinnerStyle>(() =>
+            {
+                bool hasBackground = Source.GetTexture("spinner-background") != null;
+
+                if (Source.GetTexture("spinner-top") != null && !hasBackground)
+                    return SpinnerStyle.NewLegacy;
+
+                if (hasBackground)
+                    return SpinnerStyle.OldLegacy;
+
+                return SpinnerStyle.Modern;
+            });
         }
 
         public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -110,11 +127,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         };
 
                 case OsuSkinComponents.SpinnerBody:
-                    bool hasBackground = Source.GetTexture("spinner-background") != null;
-
-                    if (Source.GetTexture("spinner-top") != null && !hasBackground)
+                    if (spinnerStyle.Value == SpinnerStyle.NewLegacy)
                         return new LegacyNewStyleSpinner();
-                    else if (hasBackground)
+                    else if (spinnerStyle.Value == SpinnerStyle.OldLegacy)
                         return new LegacyOldStyleSpinner();
 
                     return null;
@@ -151,5 +166,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             return Source.GetConfig<TLookup, TValue>(lookup);
         }
+
+        private enum SpinnerStyle
+        {
+            NewLegacy,
+            OldLegacy,
+            Modern,
+        }
     }
 }

From c441e993ff5961cba259e56b162c7ee384f0afc7 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 21:43:32 +0300
Subject: [PATCH 159/434] Separate "gained bonus" to a read-only bindable

---
 .../Objects/Drawables/DrawableSpinner.cs      | 17 +++----
 osu.Game.Rulesets.Osu/OsuSkinComponents.cs    |  3 +-
 .../Skinning/Default/SpinnerBonusDisplay.cs   | 47 -------------------
 3 files changed, 11 insertions(+), 56 deletions(-)
 delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index d02376b6c3..f16c1fc9d9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -33,12 +33,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
         public SpinnerSpmCounter SpmCounter { get; private set; }
 
         private Container<DrawableSpinnerTick> ticks;
-        private SpinnerBonusDisplay bonusDisplay;
         private PausableSkinnableSound spinningSample;
 
         private Bindable<bool> isSpinning;
         private bool spinnerFrequencyModulate;
 
+        /// <summary>
+        /// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
+        /// </summary>
+        public IBindable<double> GainedBonus => gainedBonus;
+
+        private readonly Bindable<double> gainedBonus = new Bindable<double>();
+
         public DrawableSpinner()
             : this(null)
         {
@@ -76,12 +82,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                     Y = 120,
                     Alpha = 0
                 },
-                bonusDisplay = new SpinnerBonusDisplay
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    Y = -120,
-                },
                 spinningSample = new PausableSkinnableSound
                 {
                     Volume = { Value = 0 },
@@ -293,8 +293,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                 if (tick != null)
                 {
                     tick.TriggerResult(true);
+
                     if (tick is DrawableSpinnerBonusTick)
-                        bonusDisplay.SetBonusCount(spins - HitObject.SpinsRequired);
+                        gainedBonus.Value = tick.Result.Judgement.MaxNumericResult * (spins - HitObject.SpinsRequired);
                 }
 
                 wholeSpins++;
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 2883f0c187..131645406e 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu
         SliderFollowCircle,
         SliderBall,
         SliderBody,
-        SpinnerBody
+        SpinnerBody,
+        SpinnerBonusCounter,
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs
deleted file mode 100644
index c0db6228ef..0000000000
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Osu.Objects;
-
-namespace osu.Game.Rulesets.Osu.Skinning.Default
-{
-    /// <summary>
-    /// Shows incremental bonus score achieved for a spinner.
-    /// </summary>
-    public class SpinnerBonusDisplay : CompositeDrawable
-    {
-        private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult;
-
-        private readonly OsuSpriteText bonusCounter;
-
-        public SpinnerBonusDisplay()
-        {
-            AutoSizeAxes = Axes.Both;
-
-            InternalChild = bonusCounter = new OsuSpriteText
-            {
-                Anchor = Anchor.Centre,
-                Origin = Anchor.Centre,
-                Font = OsuFont.Numeric.With(size: 24),
-                Alpha = 0,
-            };
-        }
-
-        private int displayedCount;
-
-        public void SetBonusCount(int count)
-        {
-            if (displayedCount == count)
-                return;
-
-            displayedCount = count;
-            bonusCounter.Text = $"{score_per_tick * count}";
-            bonusCounter.FadeOutFromOne(1500);
-            bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
-        }
-    }
-}

From 3f1d36ee6bced20b47a167b8780d2a736001c282 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 21:49:38 +0300
Subject: [PATCH 160/434] Add default spinner bonus counter piece

---
 .../Objects/Drawables/DrawableSpinner.cs      |  1 +
 .../Default/DefaultSpinnerBonusCounter.cs     | 51 +++++++++++++++++++
 2 files changed, 52 insertions(+)
 create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index f16c1fc9d9..4f5afc85ab 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                     Y = 120,
                     Alpha = 0
                 },
+                new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBonusCounter), _ => new DefaultSpinnerBonusCounter()),
                 spinningSample = new PausableSkinnableSound
                 {
                     Volume = { Value = 0 },
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs
new file mode 100644
index 0000000000..633766290f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+    public class DefaultSpinnerBonusCounter : CompositeDrawable
+    {
+        private OsuSpriteText bonusCounter;
+
+        private DrawableSpinner drawableSpinner;
+
+        private IBindable<double> gainedBonus;
+
+        [BackgroundDependencyLoader]
+        private void load(DrawableHitObject drawableHitObject)
+        {
+            drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+            InternalChild = bonusCounter = new OsuSpriteText
+            {
+                Alpha = 0,
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                Font = OsuFont.Numeric.With(size: 24),
+                Y = -120,
+            };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
+            gainedBonus.BindValueChanged(bonus =>
+            {
+                bonusCounter.Text = $"{bonus.NewValue}";
+                bonusCounter.FadeOutFromOne(1500);
+                bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+            });
+        }
+    }
+}

From 30f07aa9fcc22167e429be3bb798309ab3964ec4 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 2 Mar 2021 21:49:46 +0300
Subject: [PATCH 161/434] Add legacy spinner bonus counter piece

---
 .../Legacy/LegacySpinnerBonusCounter.cs       | 56 +++++++++++++++++++
 .../Legacy/OsuLegacySkinTransformer.cs        |  9 +++
 2 files changed, 65 insertions(+)
 create mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs
new file mode 100644
index 0000000000..3c4a6be4dd
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+    public class LegacySpinnerBonusCounter : CompositeDrawable
+    {
+        private LegacySpriteText bonusCounter;
+
+        private DrawableSpinner drawableSpinner;
+
+        private IBindable<double> gainedBonus;
+
+        [BackgroundDependencyLoader]
+        private void load(DrawableHitObject drawableHitObject, ISkinSource source)
+        {
+            drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+            InternalChild = new LegacyCoordinatesContainer
+            {
+                Child = bonusCounter = ((LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s =>
+                {
+                    s.Alpha = 0f;
+                    s.Anchor = Anchor.TopCentre;
+                    s.Origin = Anchor.Centre;
+                    s.Font = s.Font.With(fixedWidth: false);
+                    s.Scale = new Vector2(SPRITE_SCALE);
+                    s.Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 299;
+                }),
+            };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
+            gainedBonus.BindValueChanged(bonus =>
+            {
+                bonusCounter.Text = $"{bonus.NewValue}";
+                bonusCounter.FadeOutFromOne(800, Easing.Out);
+                bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
+            });
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index d4a403fbd2..ed09031fc1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Game.Skinning;
 using osuTK;
+using static osu.Game.Skinning.LegacySkinConfiguration;
 
 namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
@@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
         private bool hasSpinner => spinnerStyle.Value != SpinnerStyle.Modern;
 
+        private bool hasScoreFont => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ScorePrefix)?.Value ?? "score");
+
         /// <summary>
         /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
         /// Their hittable area is 128px, but the actual circle portion is 118px.
@@ -133,6 +136,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         return new LegacyOldStyleSpinner();
 
                     return null;
+
+                case OsuSkinComponents.SpinnerBonusCounter:
+                    if (hasSpinner && hasScoreFont)
+                        return new LegacySpinnerBonusCounter();
+
+                    return null;
             }
 
             return null;

From ad1b86e33a566b009ee115812c72461d87388669 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 6 Mar 2021 18:54:25 +0100
Subject: [PATCH 162/434] Change `LifetimeEnd` idiom to `Expire()` for
 readability

---
 .../Skinning/Legacy/TaikoLegacySkinTransformer.cs             | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 40dc149ec9..d97da40ef2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
             {
                 // if a taiko skin is providing explosion sprites, hide the judgements completely
                 if (hasExplosion.Value)
-                    return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
+                    return Drawable.Empty().With(d => d.Expire());
             }
 
             if (!(component is TaikoSkinComponent taikoComponent))
@@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
                     // suppress the default kiai explosion if the skin brings its own sprites.
                     // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
                     if (hasExplosion.Value)
-                        return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
+                        return Drawable.Empty().With(d => d.Expire());
 
                     return null;
 

From 3e4dfdb6755f7ea4bb721deb22029edb3dd408d0 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Sat, 6 Mar 2021 20:37:27 -0800
Subject: [PATCH 163/434] Fix pop out count being above displayed count on
 legacy combo counter

---
 osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
index 4784bca7dd..81b22b68b2 100644
--- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
@@ -84,14 +84,14 @@ namespace osu.Game.Screens.Play.HUD
         {
             InternalChildren = new[]
             {
-                displayedCountSpriteText = createSpriteText().With(s =>
-                {
-                    s.Alpha = 0;
-                }),
                 popOutCount = createSpriteText().With(s =>
                 {
                     s.Alpha = 0;
                     s.Margin = new MarginPadding(0.05f);
+                }),
+                displayedCountSpriteText = createSpriteText().With(s =>
+                {
+                    s.Alpha = 0;
                 })
             };
 

From 413cbb30a0f45b757941c053db0e983d1053d83f Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 13:39:46 +0300
Subject: [PATCH 164/434] Reword playfield shift counteract comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 5df8f8a485..9ce9fb9fd0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -152,8 +152,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                 Origin = Anchor.Centre;
                 Size = new Vector2(640, 480);
 
-                // since legacy coordinates were on screen-space, they were accounting for the playfield shift offset.
-                // therefore cancel it from here.
+                // counteracts the playfield shift from OsuPlayfieldAdjustmentContainer.
                 Position = new Vector2(0, -8f);
             }
         }

From 503f29609a69451ee2cf0de0f5473bd1939495dd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sun, 7 Mar 2021 23:40:09 +0900
Subject: [PATCH 165/434] Also set additive mode to match stable

---
 osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
index 81b22b68b2..81183a425a 100644
--- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
@@ -88,6 +88,7 @@ namespace osu.Game.Screens.Play.HUD
                 {
                     s.Alpha = 0;
                     s.Margin = new MarginPadding(0.05f);
+                    s.Blending = BlendingParameters.Additive;
                 }),
                 displayedCountSpriteText = createSpriteText().With(s =>
                 {

From fbfaa378fc25ce640eb809a0b3817511edb1042e Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 20:47:16 +0300
Subject: [PATCH 166/434] Move spinner top offset constant outside

---
 .../Skinning/Legacy/LegacyOldStyleSpinner.cs   |  2 +-
 .../Skinning/Legacy/LegacySpinner.cs           | 18 +++++++++---------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 7e9f73a89b..5c25c38504 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         // this anchor makes no sense, but that's what stable uses.
                         Anchor = Anchor.TopLeft,
                         Origin = Anchor.TopLeft,
-                        Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET },
+                        Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
                         Masking = true,
                         Child = metreSprite = new Sprite
                         {
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 9ce9fb9fd0..421c43fd7a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -16,6 +16,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
     public abstract class LegacySpinner : CompositeDrawable
     {
+        /// <summary>
+        /// An offset that simulates stable's spinner top offset, can be used with <see cref="LegacyCoordinatesContainer"/>
+        /// for positioning some legacy spinner components perfectly as in stable.
+        /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
+        /// </summary>
+        public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
+
         protected const float SPRITE_SCALE = 0.625f;
 
         protected DrawableSpinner DrawableSpinner { get; private set; }
@@ -41,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         Origin = Anchor.Centre,
                         Texture = source.GetTexture("spinner-spin"),
                         Scale = new Vector2(SPRITE_SCALE),
-                        Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335,
+                        Y = SPINNER_TOP_OFFSET + 335,
                     },
                     clear = new Sprite
                     {
@@ -50,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         Origin = Anchor.Centre,
                         Texture = source.GetTexture("spinner-clear"),
                         Scale = new Vector2(SPRITE_SCALE),
-                        Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115,
+                        Y = SPINNER_TOP_OFFSET + 115,
                     },
                 }
             });
@@ -136,13 +143,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         /// </summary>
         protected class LegacyCoordinatesContainer : Container
         {
-            /// <summary>
-            /// An offset that simulates stable's spinner top offset,
-            /// for positioning some legacy spinner components perfectly as in stable.
-            /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
-            /// </summary>
-            public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
-
             public LegacyCoordinatesContainer()
             {
                 // legacy spinners relied heavily on absolute screen-space coordinate values.

From 0ad3073c1aa3863edbfb4c4f485ad970e507127e Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 21:21:44 +0300
Subject: [PATCH 167/434] Use MathF utility class instead

Co-authored-by: Berkan Diler <b.diler@gmx.de>
---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 421c43fd7a..406c19e76a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         /// for positioning some legacy spinner components perfectly as in stable.
         /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
         /// </summary>
-        public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
+        public static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE);
 
         protected const float SPRITE_SCALE = 0.625f;
 

From d961d110bf5294e26d7d51987dce30098a53e168 Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
 <27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Mon, 8 Mar 2021 02:58:52 +0000
Subject: [PATCH 168/434] Bump Microsoft.Extensions.Configuration.Abstractions
 from 2.2.0 to 5.0.0

Bumps [Microsoft.Extensions.Configuration.Abstractions](https://github.com/dotnet/runtime) from 2.2.0 to 5.0.0.
- [Release notes](https://github.com/dotnet/runtime/releases)
- [Commits](https://github.com/dotnet/runtime/commits/v5.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
---
 osu.Game/osu.Game.csproj | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 2528292e17..c7aa6a8e11 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.302.0" />

From 74fc5d5b8cdb3452a69b70c59fa9c8c394103d3d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 13:29:09 +0900
Subject: [PATCH 169/434] Fix potential cross-thread drawable mutation in
 IntroTriangles

---
 osu.Game/Screens/BackgroundScreen.cs    | 2 ++
 osu.Game/Screens/Menu/IntroTriangles.cs | 5 +++--
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs
index 48c5523883..a6fb94b151 100644
--- a/osu.Game/Screens/BackgroundScreen.cs
+++ b/osu.Game/Screens/BackgroundScreen.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Screens
     {
         private readonly bool animateOnEnter;
 
+        public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
         protected BackgroundScreen(bool animateOnEnter = true)
         {
             this.animateOnEnter = animateOnEnter;
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index ffe6882a72..abe6c62461 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Screens.Menu
 
                 rulesets.Hide();
                 lazerLogo.Hide();
-                background.Hide();
+                background.ApplyToBackground(b => b.Hide());
 
                 using (BeginAbsoluteSequence(0, true))
                 {
@@ -231,7 +231,8 @@ namespace osu.Game.Screens.Menu
                             lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run.
 
                             logo.FadeIn();
-                            background.FadeIn();
+
+                            background.ApplyToBackground(b => b.Show());
 
                             game.Add(new GameWideFlash());
 

From 7763e1dbe130233a61493ddf1cfacab1be0f3063 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sun, 7 Mar 2021 12:39:46 +0900
Subject: [PATCH 170/434] Apply workaround for runtime iOS failures

See https://github.com/mono/mono/issues/20805#issuecomment-791440473.
---
 osu.iOS.props | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/osu.iOS.props b/osu.iOS.props
index 56a24bea12..729d692e0e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -77,12 +77,14 @@
   <PropertyGroup>
     <NoWarn>$(NoWarn);NU1605</NoWarn>
   </PropertyGroup>
+  <!-- Workaround to make SignalR 5.x work properly, avoiding a runtime error (https://github.com/mono/mono/issues/20805#issuecomment-791440473) -->
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.0.3" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.3" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
-    <PackageReference Include="MessagePack" Version="1.7.3.7" />
-    <PackageReference Include="MessagePack.Annotations" Version="2.2.85" />
+    <PackageReference Include="System.Memory" Version="4.5.4">
+      <IncludeAssets>none</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="System.Buffers" Version="4.5.1">
+      <IncludeAssets>none</IncludeAssets>
+    </PackageReference>
   </ItemGroup>
   <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
   <ItemGroup Label="Transitive Dependencies">

From 765cc5cf37120b9892e9233127573c5189b92456 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 13:29:47 +0900
Subject: [PATCH 171/434] Remove iOS multiplayer blocking code

---
 osu.Game/Online/API/APIAccess.cs      | 11 ++---------
 osu.Game/Screens/Menu/ButtonSystem.cs | 12 ------------
 2 files changed, 2 insertions(+), 21 deletions(-)

diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 569481d491..ede64c0340 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -10,7 +10,6 @@ using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Newtonsoft.Json.Linq;
-using osu.Framework;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.ExceptionExtensions;
 using osu.Framework.Extensions.ObjectExtensions;
@@ -247,14 +246,8 @@ namespace osu.Game.Online.API
             this.password = password;
         }
 
-        public IHubClientConnector GetHubConnector(string clientName, string endpoint)
-        {
-            // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
-            if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
-                return null;
-
-            return new HubClientConnector(clientName, endpoint, this, versionHash);
-        }
+        public IHubClientConnector GetHubConnector(string clientName, string endpoint) =>
+            new HubClientConnector(clientName, endpoint, this, versionHash);
 
         public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
         {
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index f93bfd7705..81b1cb0bf1 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -172,18 +172,6 @@ namespace osu.Game.Screens.Menu
                 return;
             }
 
-            // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
-            if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
-            {
-                notifications?.Post(new SimpleNotification
-                {
-                    Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.",
-                    Icon = FontAwesome.Solid.AppleAlt,
-                });
-
-                return;
-            }
-
             OnMultiplayer?.Invoke();
         }
 

From b1cd01ceb82b9d04ea0c20b6a15392b2413f6bc4 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 12:57:16 +0900
Subject: [PATCH 172/434] Apply ConfigureAwait changes to game side

---
 CodeAnalysis/osu.ruleset                      |  2 +-
 osu.Desktop/Updater/SquirrelUpdateManager.cs  | 14 ++++++------
 osu.Game/Beatmaps/BeatmapManager.cs           |  2 +-
 osu.Game/Collections/CollectionManager.cs     |  4 ++--
 osu.Game/Database/ArchiveModelManager.cs      | 15 ++++++-------
 .../DownloadableArchiveModelManager.cs        |  2 +-
 osu.Game/Database/MemoryCachingComponent.cs   |  2 +-
 osu.Game/Database/UserLookupCache.cs          |  2 +-
 osu.Game/Graphics/ScreenshotManager.cs        |  6 ++---
 osu.Game/IO/Archives/ArchiveReader.cs         |  2 +-
 osu.Game/IPC/ArchiveImportIPCChannel.cs       |  4 ++--
 osu.Game/Online/HubClientConnector.cs         | 12 +++++-----
 .../Multiplayer/StatefulMultiplayerClient.cs  | 22 +++++++++----------
 osu.Game/OsuGame.cs                           |  8 +++----
 osu.Game/OsuGameBase.cs                       |  4 ++--
 osu.Game/Overlays/ChangelogOverlay.cs         |  4 ++--
 osu.Game/Scoring/ScorePerformanceCache.cs     |  2 +-
 osu.Game/Screens/Import/FileImportScreen.cs   |  2 +-
 .../Multiplayer/MultiplayerPlayer.cs          |  6 ++---
 .../OnlinePlay/Playlists/PlaylistsPlayer.cs   |  4 ++--
 osu.Game/Screens/Play/Player.cs               |  4 ++--
 osu.Game/Skinning/SkinManager.cs              |  2 +-
 .../Multiplayer/TestMultiplayerClient.cs      |  2 +-
 osu.Game/Updater/SimpleUpdateManager.cs       |  2 +-
 osu.Game/Updater/UpdateManager.cs             |  2 +-
 25 files changed, 65 insertions(+), 66 deletions(-)

diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset
index d497365f87..6a99e230d1 100644
--- a/CodeAnalysis/osu.ruleset
+++ b/CodeAnalysis/osu.ruleset
@@ -30,7 +30,7 @@
     <Rule Id="CA1819" Action="None" />
     <Rule Id="CA1822" Action="None" />
     <Rule Id="CA1823" Action="None" />
-    <Rule Id="CA2007" Action="None" />
+    <Rule Id="CA2007" Action="Warning" />
     <Rule Id="CA2214" Action="None" />
     <Rule Id="CA2227" Action="None" />
   </Rules>
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 71f9fafe57..47cd39dc5a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -42,7 +42,7 @@ namespace osu.Desktop.Updater
             Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
         }
 
-        protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
+        protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
 
         private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
         {
@@ -51,9 +51,9 @@ namespace osu.Desktop.Updater
 
             try
             {
-                updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
+                updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
 
-                var info = await updateManager.CheckForUpdate(!useDeltaPatching);
+                var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
 
                 if (info.ReleasesToApply.Count == 0)
                 {
@@ -79,12 +79,12 @@ namespace osu.Desktop.Updater
 
                 try
                 {
-                    await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f);
+                    await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
 
                     notification.Progress = 0;
                     notification.Text = @"Installing update...";
 
-                    await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
+                    await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
 
                     notification.State = ProgressNotificationState.Completed;
                     updatePending = true;
@@ -97,7 +97,7 @@ namespace osu.Desktop.Updater
 
                         // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
                         // try again without deltas.
-                        await checkForUpdateAsync(false, notification);
+                        await checkForUpdateAsync(false, notification).ConfigureAwait(false);
                         scheduleRecheck = false;
                     }
                     else
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
                 if (scheduleRecheck)
                 {
                     // check again in 30 minutes.
-                    Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
+                    Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
                 }
             }
 
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index d653e5386b..29b3f5d3a3 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps
             bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
 
             if (onlineLookupQueue != null)
-                await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
+                await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
 
             // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
             if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs
index fb9c230c7a..9723409c79 100644
--- a/osu.Game/Collections/CollectionManager.cs
+++ b/osu.Game/Collections/CollectionManager.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Collections
             return Task.Run(async () =>
             {
                 using (var stream = stable.GetStream(database_name))
-                    await Import(stream);
+                    await Import(stream).ConfigureAwait(false);
             });
         }
 
@@ -139,7 +139,7 @@ namespace osu.Game.Collections
             PostNotification?.Invoke(notification);
 
             var collections = readCollections(stream, notification);
-            await importCollections(collections);
+            await importCollections(collections).ConfigureAwait(false);
 
             notification.CompletionText = $"Imported {collections.Count} collections";
             notification.State = ProgressNotificationState.Completed;
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index daaba9098e..d809dbcb01 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -22,7 +22,6 @@ using osu.Game.IO.Archives;
 using osu.Game.IPC;
 using osu.Game.Overlays.Notifications;
 using SharpCompress.Archives.Zip;
-using FileInfo = osu.Game.IO.FileInfo;
 
 namespace osu.Game.Database
 {
@@ -163,7 +162,7 @@ namespace osu.Game.Database
 
                 try
                 {
-                    var model = await Import(task, isLowPriorityImport, notification.CancellationToken);
+                    var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
 
                     lock (imported)
                     {
@@ -183,7 +182,7 @@ namespace osu.Game.Database
                 {
                     Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
                 }
-            }));
+            })).ConfigureAwait(false);
 
             if (imported.Count == 0)
             {
@@ -226,7 +225,7 @@ namespace osu.Game.Database
 
             TModel import;
             using (ArchiveReader reader = task.GetReader())
-                import = await Import(reader, lowPriority, cancellationToken);
+                import = await Import(reader, lowPriority, 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.
@@ -358,7 +357,7 @@ namespace osu.Game.Database
                 item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
                 item.Hash = ComputeHash(item, archive);
 
-                await Populate(item, archive, cancellationToken);
+                await Populate(item, archive, cancellationToken).ConfigureAwait(false);
 
                 using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
                 {
@@ -410,7 +409,7 @@ namespace osu.Game.Database
 
             flushEvents(true);
             return item;
-        }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap();
+        }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false);
 
         /// <summary>
         /// Exports an item to a legacy (.zip based) package.
@@ -621,7 +620,7 @@ namespace osu.Game.Database
         }
 
         /// <summary>
-        /// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
+        /// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store.
         /// </summary>
         private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files)
         {
@@ -699,7 +698,7 @@ namespace osu.Game.Database
                 return Task.CompletedTask;
             }
 
-            return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
+            return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false));
         }
 
         /// <summary>
diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs
index 50b022f9ff..da3144e8d0 100644
--- a/osu.Game/Database/DownloadableArchiveModelManager.cs
+++ b/osu.Game/Database/DownloadableArchiveModelManager.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Database
                 Task.Factory.StartNew(async () =>
                 {
                     // This gets scheduled back to the update thread, but we want the import to run in the background.
-                    var imported = await Import(notification, new ImportTask(filename));
+                    var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false);
 
                     // for now a failed import will be marked as a failed download for simplicity.
                     if (!imported.Any())
diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs
index d913e66428..a1a1279d71 100644
--- a/osu.Game/Database/MemoryCachingComponent.cs
+++ b/osu.Game/Database/MemoryCachingComponent.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Database
             if (CheckExists(lookup, out TValue performance))
                 return performance;
 
-            var computed = await ComputeValueAsync(lookup, token);
+            var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
 
             if (computed != null || CacheNullValues)
                 cache[lookup] = computed;
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index 568726199c..19cc211709 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Database
         public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
 
         protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
-            => await queryUser(lookup);
+            => await queryUser(lookup).ConfigureAwait(false);
 
         private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
         private Task pendingRequestTask;
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index f7914cbbca..fb7fe4947b 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Graphics
                 }
             }
 
-            using (var image = await host.TakeScreenshotAsync())
+            using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
             {
                 if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
                     cursorVisibility.Value = true;
@@ -116,13 +116,13 @@ namespace osu.Game.Graphics
                 switch (screenshotFormat.Value)
                 {
                     case ScreenshotFormat.Png:
-                        await image.SaveAsPngAsync(stream);
+                        await image.SaveAsPngAsync(stream).ConfigureAwait(false);
                         break;
 
                     case ScreenshotFormat.Jpg:
                         const int jpeg_quality = 92;
 
-                        await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality });
+                        await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
                         break;
 
                     default:
diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs
index f74574e60c..679ab40402 100644
--- a/osu.Game/IO/Archives/ArchiveReader.cs
+++ b/osu.Game/IO/Archives/ArchiveReader.cs
@@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives
                     return null;
 
                 byte[] buffer = new byte[input.Length];
-                await input.ReadAsync(buffer);
+                await input.ReadAsync(buffer).ConfigureAwait(false);
                 return buffer;
             }
         }
diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs
index 029908ec9d..d9d0e4c0ea 100644
--- a/osu.Game/IPC/ArchiveImportIPCChannel.cs
+++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs
@@ -33,12 +33,12 @@ namespace osu.Game.IPC
             if (importer == null)
             {
                 // we want to contact a remote osu! to handle the import.
-                await SendMessageAsync(new ArchiveImportMessage { Path = path });
+                await SendMessageAsync(new ArchiveImportMessage { Path = path }).ConfigureAwait(false);
                 return;
             }
 
             if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant()))
-                await importer.Import(path);
+                await importer.Import(path).ConfigureAwait(false);
         }
     }
 
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index fdb21c5000..3839762e46 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Online
         {
             cancelExistingConnect();
 
-            if (!await connectionLock.WaitAsync(10000))
+            if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
                 throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
 
             try
@@ -88,7 +88,7 @@ namespace osu.Game.Online
                 {
                     // ensure any previous connection was disposed.
                     // this will also create a new cancellation token source.
-                    await disconnect(false);
+                    await disconnect(false).ConfigureAwait(false);
 
                     // this token will be valid for the scope of this connection.
                     // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
@@ -103,7 +103,7 @@ namespace osu.Game.Online
                         // importantly, rebuild the connection each attempt to get an updated access token.
                         CurrentConnection = buildConnection(cancellationToken);
 
-                        await CurrentConnection.StartAsync(cancellationToken);
+                        await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
 
                         Logger.Log($"{clientName} connected!", LoggingTarget.Network);
                         isConnected.Value = true;
@@ -119,7 +119,7 @@ namespace osu.Game.Online
                         Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
 
                         // retry on any failure.
-                        await Task.Delay(5000, cancellationToken);
+                        await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
                     }
                 }
             }
@@ -174,14 +174,14 @@ namespace osu.Game.Online
 
             if (takeLock)
             {
-                if (!await connectionLock.WaitAsync(10000))
+                if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
                     throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
             }
 
             try
             {
                 if (CurrentConnection != null)
-                    await CurrentConnection.DisposeAsync();
+                    await CurrentConnection.DisposeAsync().ConfigureAwait(false);
             }
             finally
             {
diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
index 73100be505..0f7050596f 100644
--- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs
@@ -131,12 +131,12 @@ namespace osu.Game.Online.Multiplayer
                 Debug.Assert(room.RoomID.Value != null);
 
                 // Join the server-side room.
-                var joinedRoom = await JoinRoom(room.RoomID.Value.Value);
+                var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
                 Debug.Assert(joinedRoom != null);
 
                 // Populate users.
                 Debug.Assert(joinedRoom.Users != null);
-                await Task.WhenAll(joinedRoom.Users.Select(PopulateUser));
+                await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
 
                 // Update the stored room (must be done on update thread for thread-safety).
                 await scheduleAsync(() =>
@@ -144,11 +144,11 @@ namespace osu.Game.Online.Multiplayer
                     Room = joinedRoom;
                     apiRoom = room;
                     defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
-                }, cancellationSource.Token);
+                }, cancellationSource.Token).ConfigureAwait(false);
 
                 // Update room settings.
-                await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token);
-            }, cancellationSource.Token);
+                await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false);
+            }, cancellationSource.Token).ConfigureAwait(false);
         }
 
         /// <summary>
@@ -178,8 +178,8 @@ namespace osu.Game.Online.Multiplayer
 
             return joinOrLeaveTaskChain.Add(async () =>
             {
-                await scheduledReset;
-                await LeaveRoomInternal();
+                await scheduledReset.ConfigureAwait(false);
+                await LeaveRoomInternal().ConfigureAwait(false);
             });
         }
 
@@ -237,11 +237,11 @@ namespace osu.Game.Online.Multiplayer
             switch (localUser.State)
             {
                 case MultiplayerUserState.Idle:
-                    await ChangeState(MultiplayerUserState.Ready);
+                    await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
                     return;
 
                 case MultiplayerUserState.Ready:
-                    await ChangeState(MultiplayerUserState.Idle);
+                    await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
                     return;
 
                 default:
@@ -307,7 +307,7 @@ namespace osu.Game.Online.Multiplayer
             if (Room == null)
                 return;
 
-            await PopulateUser(user);
+            await PopulateUser(user).ConfigureAwait(false);
 
             Scheduler.Add(() =>
             {
@@ -486,7 +486,7 @@ namespace osu.Game.Online.Multiplayer
         /// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
         /// </summary>
         /// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
-        protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
+        protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
 
         /// <summary>
         /// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 203cc458e0..b7398efdc2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -440,7 +440,7 @@ namespace osu.Game
         public override Task Import(params ImportTask[] imports)
         {
             // encapsulate task as we don't want to begin the import process until in a ready state.
-            var importTask = new Task(async () => await base.Import(imports));
+            var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
 
             waitForReady(() => this, _ => importTask.Start());
 
@@ -831,7 +831,7 @@ namespace osu.Game
                 asyncLoadStream = Task.Run(async () =>
                 {
                     if (previousLoadStream != null)
-                        await previousLoadStream;
+                        await previousLoadStream.ConfigureAwait(false);
 
                     try
                     {
@@ -845,7 +845,7 @@ namespace osu.Game
 
                         // The delegate won't complete if OsuGame has been disposed in the meantime
                         while (!IsDisposed && !del.Completed)
-                            await Task.Delay(10);
+                            await Task.Delay(10).ConfigureAwait(false);
 
                         // Either we're disposed or the load process has started successfully
                         if (IsDisposed)
@@ -853,7 +853,7 @@ namespace osu.Game
 
                         Debug.Assert(task != null);
 
-                        await task;
+                        await task.ConfigureAwait(false);
 
                         Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
                     }
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 3d24f245f9..e1c7b67a8c 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -434,7 +434,7 @@ namespace osu.Game
             foreach (var importer in fileImporters)
             {
                 if (importer.HandledExtensions.Contains(extension))
-                    await importer.Import(paths);
+                    await importer.Import(paths).ConfigureAwait(false);
             }
         }
 
@@ -445,7 +445,7 @@ namespace osu.Game
             {
                 var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
                 return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
-            }));
+            })).ConfigureAwait(false);
         }
 
         public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 537dd00727..2da5be5e6c 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -160,9 +160,9 @@ namespace osu.Game.Overlays
                     tcs.SetException(e);
                 };
 
-                await API.PerformAsync(req);
+                await API.PerformAsync(req).ConfigureAwait(false);
 
-                await tcs.Task;
+                return tcs.Task;
             });
         }
 
diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs
index 5f66c13d2f..bb15983de3 100644
--- a/osu.Game/Scoring/ScorePerformanceCache.cs
+++ b/osu.Game/Scoring/ScorePerformanceCache.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Scoring
         {
             var score = lookup.ScoreInfo;
 
-            var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token);
+            var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false);
 
             // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
             if (attributes.Attributes == null)
diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs
index 329623e03a..ee8ef6926d 100644
--- a/osu.Game/Screens/Import/FileImportScreen.cs
+++ b/osu.Game/Screens/Import/FileImportScreen.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Screens.Import
 
             Task.Factory.StartNew(async () =>
             {
-                await game.Import(path);
+                await game.Import(path).ConfigureAwait(false);
 
                 // some files will be deleted after successful import, so we want to refresh the view.
                 Schedule(() =>
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index ffcf248575..b3cd44d55a 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -137,13 +137,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
         protected override async Task SubmitScore(Score score)
         {
-            await base.SubmitScore(score);
+            await base.SubmitScore(score).ConfigureAwait(false);
 
-            await client.ChangeState(MultiplayerUserState.FinishedPlay);
+            await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false);
 
             // Await up to 60 seconds for results to become available (6 api request timeouts).
             // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
-            await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60)));
+            await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false);
         }
 
         protected override ResultsScreen CreateResults(ScoreInfo score)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index ddc88261f7..a75e4bdc07 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
 
         protected override async Task SubmitScore(Score score)
         {
-            await base.SubmitScore(score);
+            await base.SubmitScore(score).ConfigureAwait(false);
 
             Debug.Assert(Token != null);
 
@@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
             };
 
             api.Queue(request);
-            await tcs.Task;
+            await tcs.Task.ConfigureAwait(false);
         }
 
         protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index e81efdac78..0e221351aa 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -592,7 +592,7 @@ namespace osu.Game.Screens.Play
 
                 try
                 {
-                    await SubmitScore(score);
+                    await SubmitScore(score).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
@@ -601,7 +601,7 @@ namespace osu.Game.Screens.Play
 
                 try
                 {
-                    await ImportScore(score);
+                    await ImportScore(score).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 2826c826a5..fcde9f041b 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Skinning
 
         protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
         {
-            await base.Populate(model, archive, cancellationToken);
+            await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
 
             if (model.Name?.Contains(".osk") == true)
                 populateMetadata(model);
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index c03364a391..09fcc1ff47 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Debug.Assert(Room != null);
 
-            await ((IMultiplayerClient)this).SettingsChanged(settings);
+            await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
 
             foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
                 ChangeUserState(user.UserID, MultiplayerUserState.Idle);
diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
index 4ebf2a7368..6eded7ce53 100644
--- a/osu.Game/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Updater
             {
                 var releases = new OsuJsonWebRequest<GitHubRelease>("https://api.github.com/repos/ppy/osu/releases/latest");
 
-                await releases.PerformAsync();
+                await releases.PerformAsync().ConfigureAwait(false);
 
                 var latest = releases.ResponseObject;
 
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index f772c6d282..9a0454bc95 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Updater
             lock (updateTaskLock)
                 waitTask = (updateCheckTask ??= PerformUpdateCheck());
 
-            bool hasUpdates = await waitTask;
+            bool hasUpdates = await waitTask.ConfigureAwait(false);
 
             lock (updateTaskLock)
                 updateCheckTask = null;

From d2bc48e57650d0ff2c1ec9cf840f4258f90b786b Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 12:57:30 +0900
Subject: [PATCH 173/434] Exclude tests from ConfigureAwait rule

---
 osu.Game.Tests/osu.Game.Tests.csproj | 3 +++
 osu.Game.Tests/tests.ruleset         | 6 ++++++
 2 files changed, 9 insertions(+)
 create mode 100644 osu.Game.Tests/tests.ruleset

diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 32ccb5b699..e36b3cdc74 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -13,6 +13,9 @@
     <OutputType>WinExe</OutputType>
     <TargetFramework>net5.0</TargetFramework>
   </PropertyGroup>
+  <PropertyGroup Label="Code Analysis">
+    <CodeAnalysisRuleSet>tests.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
   <ItemGroup Label="Project References">
     <ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
     <ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
diff --git a/osu.Game.Tests/tests.ruleset b/osu.Game.Tests/tests.ruleset
new file mode 100644
index 0000000000..a0abb781d3
--- /dev/null
+++ b/osu.Game.Tests/tests.ruleset
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0">
+    <Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
+        <Rule Id="CA2007" Action="None" />
+    </Rules>
+</RuleSet>

From 6cb0db9c33c41312e9a380168d41327794a2288c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 14:45:11 +0900
Subject: [PATCH 174/434] Apply override rules to iOS/Android test projects

---
 osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 5 ++++-
 osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj         | 5 ++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index 543f2f35a7..c3d9cb5875 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -20,6 +20,9 @@
   <ItemGroup>
     <None Include="Properties\AndroidManifest.xml" />
   </ItemGroup>
+  <PropertyGroup>
+    <NoWarn>$(NoWarn);CA2007</NoWarn>
+  </PropertyGroup>
   <ItemGroup>
     <Compile Include="..\osu.Game.Tests\**\Beatmaps\**\*.cs">
       <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
@@ -74,4 +77,4 @@
     <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
-</Project>
\ No newline at end of file
+</Project>
diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
index e83bef4a95..97df9b2cd5 100644
--- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
+++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj
@@ -21,6 +21,9 @@
       <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
     </Compile>
   </ItemGroup>
+  <PropertyGroup>
+    <NoWarn>$(NoWarn);CA2007</NoWarn>
+  </PropertyGroup>
   <ItemGroup Label="Project References">
     <ProjectReference Include="..\osu.Game\osu.Game.csproj">
       <Project>{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}</Project>
@@ -48,4 +51,4 @@
     <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
-</Project>
\ No newline at end of file
+</Project>

From 02194a93cb324e9a3781e82a73ce47ae6ce40f45 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 8 Mar 2021 15:17:10 +0900
Subject: [PATCH 175/434] Apply missing additions to android project

---
 osu.Android/OsuGameActivity.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index d087c6218d..cffcea22c2 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -100,15 +100,15 @@ namespace osu.Android
                 // copy to an arbitrary-access memory stream to be able to proceed with the import.
                 var copy = new MemoryStream();
                 using (var stream = ContentResolver.OpenInputStream(uri))
-                    await stream.CopyToAsync(copy);
+                    await stream.CopyToAsync(copy).ConfigureAwait(false);
 
                 lock (tasks)
                 {
                     tasks.Add(new ImportTask(copy, filename));
                 }
-            }));
+            })).ConfigureAwait(false);
 
-            await game.Import(tasks.ToArray());
+            await game.Import(tasks.ToArray()).ConfigureAwait(false);
         }, TaskCreationOptions.LongRunning);
     }
 }

From bb79da1aacfd45dc73d1f493bd5e0400327dc61f Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 9 Mar 2021 00:33:43 +0300
Subject: [PATCH 176/434] Correct playfield shift counteract comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 406c19e76a..896c3f4a3e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -152,7 +152,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                 Origin = Anchor.Centre;
                 Size = new Vector2(640, 480);
 
-                // counteracts the playfield shift from OsuPlayfieldAdjustmentContainer.
+                // stable applies this adjustment conditionally, locally in the spinner.
+                // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer,
+                // therefore it's safe to apply it unconditionally in this component.
                 Position = new Vector2(0, -8f);
             }
         }

From dc9028d24acd3df3152057f36f0aa0217b9c954a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 9 Mar 2021 14:27:20 +0900
Subject: [PATCH 177/434] Update framework

---
 osu.Android.props                       | 2 +-
 osu.Game/Updater/SimpleUpdateManager.cs | 2 +-
 osu.Game/osu.Game.csproj                | 2 +-
 osu.iOS.props                           | 4 ++--
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index c428cd2546..5b700224db 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.302.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.309.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
index 6eded7ce53..50572a7867 100644
--- a/osu.Game/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Updater
                     bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal));
                     break;
 
-                case RuntimeInfo.Platform.MacOsx:
+                case RuntimeInfo.Platform.macOS:
                     bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal));
                     break;
 
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c7aa6a8e11..90c8b98f42 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.302.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 729d692e0e..ccd33bf88c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.302.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -93,7 +93,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.302.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 05493958696d488c4a8b76582ee0f92d5e7cf75b Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Tue, 9 Mar 2021 08:55:32 +0300
Subject: [PATCH 178/434] Inline "legacy coordinates container" and add
 "spinner Y centre" const

---
 .../Skinning/Legacy/LegacyNewStyleSpinner.cs  |  3 +-
 .../Skinning/Legacy/LegacyOldStyleSpinner.cs  | 33 ++++----
 .../Skinning/Legacy/LegacySpinner.cs          | 82 ++++++++-----------
 3 files changed, 50 insertions(+), 68 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index efeca53969..22fb3aab86 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             AddInternal(scaleContainer = new Container
             {
                 Scale = new Vector2(SPRITE_SCALE),
-                Anchor = Anchor.Centre,
+                Anchor = Anchor.TopCentre,
                 Origin = Anchor.Centre,
                 RelativeSizeAxes = Axes.Both,
+                Y = SPINNER_Y_CENTRE,
                 Children = new Drawable[]
                 {
                     glow = new Sprite
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 5c25c38504..19cb55c16e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -37,35 +37,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             {
                 new Sprite
                 {
-                    Anchor = Anchor.Centre,
+                    Anchor = Anchor.TopCentre,
                     Origin = Anchor.Centre,
                     Texture = source.GetTexture("spinner-background"),
-                    Scale = new Vector2(SPRITE_SCALE)
+                    Scale = new Vector2(SPRITE_SCALE),
+                    Y = SPINNER_Y_CENTRE,
                 },
                 disc = new Sprite
                 {
-                    Anchor = Anchor.Centre,
+                    Anchor = Anchor.TopCentre,
                     Origin = Anchor.Centre,
                     Texture = source.GetTexture("spinner-circle"),
-                    Scale = new Vector2(SPRITE_SCALE)
+                    Scale = new Vector2(SPRITE_SCALE),
+                    Y = SPINNER_Y_CENTRE,
                 },
-                new LegacyCoordinatesContainer
+                metre = new Container
                 {
-                    Child = metre = new Container
+                    AutoSizeAxes = Axes.Both,
+                    // this anchor makes no sense, but that's what stable uses.
+                    Anchor = Anchor.TopLeft,
+                    Origin = Anchor.TopLeft,
+                    Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
+                    Masking = true,
+                    Child = metreSprite = new Sprite
                     {
-                        AutoSizeAxes = Axes.Both,
-                        // this anchor makes no sense, but that's what stable uses.
+                        Texture = source.GetTexture("spinner-metre"),
                         Anchor = Anchor.TopLeft,
                         Origin = Anchor.TopLeft,
-                        Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
-                        Masking = true,
-                        Child = metreSprite = new Sprite
-                        {
-                            Texture = source.GetTexture("spinner-metre"),
-                            Anchor = Anchor.TopLeft,
-                            Origin = Anchor.TopLeft,
-                            Scale = new Vector2(SPRITE_SCALE)
-                        }
+                        Scale = new Vector2(SPRITE_SCALE)
                     }
                 }
             });
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 896c3f4a3e..1738003390 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -16,12 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
     public abstract class LegacySpinner : CompositeDrawable
     {
-        /// <summary>
-        /// An offset that simulates stable's spinner top offset, can be used with <see cref="LegacyCoordinatesContainer"/>
-        /// for positioning some legacy spinner components perfectly as in stable.
-        /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
-        /// </summary>
-        public static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE);
+        protected static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE);
+        protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
 
         protected const float SPRITE_SCALE = 0.625f;
 
@@ -33,33 +29,41 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         [BackgroundDependencyLoader]
         private void load(DrawableHitObject drawableHitObject, ISkinSource source)
         {
-            RelativeSizeAxes = Axes.Both;
+            // legacy spinners relied heavily on absolute screen-space coordinate values.
+            // wrap everything in a container simulating absolute coords to preserve alignment
+            // as there are skins that depend on it.
+            Anchor = Anchor.Centre;
+            Origin = Anchor.Centre;
+            Size = new Vector2(640, 480);
+
+            // stable applies this adjustment conditionally, locally in the spinner.
+            // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer,
+            // therefore it's safe to apply it unconditionally in this component.
+            Position = new Vector2(0, -8f);
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            AddInternal(new LegacyCoordinatesContainer
+            AddRangeInternal(new[]
             {
-                Depth = float.MinValue,
-                Children = new Drawable[]
+                spin = new Sprite
                 {
-                    spin = new Sprite
-                    {
-                        Anchor = Anchor.TopCentre,
-                        Origin = Anchor.Centre,
-                        Texture = source.GetTexture("spinner-spin"),
-                        Scale = new Vector2(SPRITE_SCALE),
-                        Y = SPINNER_TOP_OFFSET + 335,
-                    },
-                    clear = new Sprite
-                    {
-                        Alpha = 0,
-                        Anchor = Anchor.TopCentre,
-                        Origin = Anchor.Centre,
-                        Texture = source.GetTexture("spinner-clear"),
-                        Scale = new Vector2(SPRITE_SCALE),
-                        Y = SPINNER_TOP_OFFSET + 115,
-                    },
-                }
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.Centre,
+                    Depth = float.MinValue,
+                    Texture = source.GetTexture("spinner-spin"),
+                    Scale = new Vector2(SPRITE_SCALE),
+                    Y = SPINNER_TOP_OFFSET + 335,
+                },
+                clear = new Sprite
+                {
+                    Alpha = 0,
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.Centre,
+                    Depth = float.MinValue,
+                    Texture = source.GetTexture("spinner-clear"),
+                    Scale = new Vector2(SPRITE_SCALE),
+                    Y = SPINNER_TOP_OFFSET + 115,
+                },
             });
         }
 
@@ -136,27 +140,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             if (DrawableSpinner != null)
                 DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms;
         }
-
-        /// <summary>
-        /// A <see cref="Container"/> simulating osu!stable's absolute screen-space,
-        /// for perfect placements of legacy spinner components with legacy coordinates.
-        /// </summary>
-        protected class LegacyCoordinatesContainer : Container
-        {
-            public LegacyCoordinatesContainer()
-            {
-                // legacy spinners relied heavily on absolute screen-space coordinate values.
-                // wrap everything in a container simulating absolute coords to preserve alignment
-                // as there are skins that depend on it.
-                Anchor = Anchor.Centre;
-                Origin = Anchor.Centre;
-                Size = new Vector2(640, 480);
-
-                // stable applies this adjustment conditionally, locally in the spinner.
-                // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer,
-                // therefore it's safe to apply it unconditionally in this component.
-                Position = new Vector2(0, -8f);
-            }
-        }
     }
 }

From a5b3ac7ef8101c867bec8c1188ec4595ccb1c919 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 9 Mar 2021 15:45:03 +0900
Subject: [PATCH 179/434] Add failing test covering alpha commands proceeding
 non-alpha (but ignored) commands

---
 .../Visual/Gameplay/TestSceneLeadIn.cs        | 40 ++++++++++++++++++-
 1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index 563d6be0da..dccde366c2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
@@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay
         [TestCase(0, 0)]
         [TestCase(-1000, -1000)]
         [TestCase(-10000, -10000)]
-        public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime)
+        public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
         {
             var storyboard = new Storyboard();
 
             var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
+
             sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
 
             storyboard.GetLayer("Background").Add(sprite);
@@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay
             });
         }
 
+        [TestCase(1000, 0, false)]
+        [TestCase(0, 0, false)]
+        [TestCase(-1000, -1000, false)]
+        [TestCase(-10000, -10000, false)]
+        [TestCase(1000, 0, true)]
+        [TestCase(0, 0, true)]
+        [TestCase(-1000, -1000, true)]
+        [TestCase(-10000, -10000, true)]
+        public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
+        {
+            var storyboard = new Storyboard();
+
+            var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
+
+            // these should be ignored as we have an alpha visibility blocker proceeding this command.
+            sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
+            var loopGroup = sprite.AddLoop(-20000, 50);
+            loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
+
+            var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
+            target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
+
+            // these should be ignored due to being in the future.
+            sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
+            loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
+
+            storyboard.GetLayer("Background").Add(sprite);
+
+            loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
+
+            AddAssert($"first frame is {expectedStartTime}", () =>
+            {
+                Debug.Assert(player.FirstFrameClockTime != null);
+                return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
+            });
+        }
+
         private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
         {
             AddStep("create player", () =>

From 8aaba324314c4cfc628de35a3b6ab438227103a5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 9 Mar 2021 15:55:05 +0900
Subject: [PATCH 180/434] Fix storyboard commands occurring before the earliest
 point of visibility delaying gameplay

In osu-stable, storyboard intros start from the first command, but in
the case of storyboard drawables which have an initial hidden state, all
commands before the time at which they become visible (ie. the first
command where `Alpha` increases to a non-zero value) are ignored.

This brings lazer in line with that behaviour. It also removes several
unnecessary LINQ calls.

Note that the alpha check being done in its own pass is important, as
it must be the "minimum present alpha across all command groups,
including loops". This is what makes the implementation slightly
complex.

Closes #11981.
---
 osu.Game/Storyboards/CommandTimelineGroup.cs | 19 +++++++++
 osu.Game/Storyboards/StoryboardSprite.cs     | 45 +++++++++++++++++---
 2 files changed, 58 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs
index 6ce3b617e9..617455cf0b 100644
--- a/osu.Game/Storyboards/CommandTimelineGroup.cs
+++ b/osu.Game/Storyboards/CommandTimelineGroup.cs
@@ -45,11 +45,30 @@ namespace osu.Game.Storyboards
             };
         }
 
+        /// <summary>
+        /// Returns the earliest visible time. Will be null unless this group has an <see cref="Alpha"/> command with a start value of zero.
+        /// </summary>
+        public double? EarliestDisplayedTime
+        {
+            get
+            {
+                var first = Alpha.Commands.FirstOrDefault();
+
+                return first?.StartValue == 0 ? first.StartTime : (double?)null;
+            }
+        }
+
         [JsonIgnore]
         public double CommandsStartTime
         {
             get
             {
+                // if the first alpha command starts at zero it should be given priority over anything else.
+                // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible.
+                var earliestDisplay = EarliestDisplayedTime;
+                if (earliestDisplay != null)
+                    return earliestDisplay.Value;
+
                 double min = double.MaxValue;
 
                 for (int i = 0; i < timelines.Length; i++)
diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs
index f411ad04f3..fdaa59d7d9 100644
--- a/osu.Game/Storyboards/StoryboardSprite.cs
+++ b/osu.Game/Storyboards/StoryboardSprite.cs
@@ -24,13 +24,46 @@ namespace osu.Game.Storyboards
 
         public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
 
-        public double StartTime => Math.Min(
-            TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue,
-            loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue);
+        public double StartTime
+        {
+            get
+            {
+                // check for presence affecting commands as an initial pass.
+                double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue;
 
-        public double EndTime => Math.Max(
-            TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue,
-            loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue);
+                foreach (var l in loops)
+                {
+                    if (!(l.EarliestDisplayedTime is double lEarliest))
+                        continue;
+
+                    earliestStartTime = Math.Min(earliestStartTime, lEarliest);
+                }
+
+                if (earliestStartTime < double.MaxValue)
+                    return earliestStartTime;
+
+                // if an alpha-affecting command was not found, use the earliest of any command.
+                earliestStartTime = TimelineGroup.StartTime;
+
+                foreach (var l in loops)
+                    earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
+
+                return earliestStartTime;
+            }
+        }
+
+        public double EndTime
+        {
+            get
+            {
+                double latestEndTime = TimelineGroup.EndTime;
+
+                foreach (var l in loops)
+                    latestEndTime = Math.Max(latestEndTime, l.EndTime);
+
+                return latestEndTime;
+            }
+        }
 
         public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
 

From 5a6864eb7826502cc74132275206834bf81532fe Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 9 Mar 2021 16:43:44 +0900
Subject: [PATCH 181/434] Fix SPM counter immediately disappearing on
 completion of spinners

---
 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs  | 3 +++
 osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs | 5 +++++
 2 files changed, 8 insertions(+)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index d02376b6c3..69095fd160 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -109,6 +109,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             base.OnFree();
 
             spinningSample.Samples = null;
+
+            // the counter handles its own fade in (when spinning begins) so we should only be responsible for resetting it here, for pooling.
+            SpmCounter.Hide();
         }
 
         protected override void LoadSamples()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
index 69355f624b..f3e013c759 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
@@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
 
         private readonly OsuSpriteText spmText;
 
+        public override void ApplyTransformsAt(double time, bool propagateChildren = false)
+        {
+            // handles own fade in state.
+        }
+
         public SpinnerSpmCounter()
         {
             Children = new Drawable[]

From 4e8bcc92659b47bb1223e43458f8159475e78209 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 9 Mar 2021 16:15:44 +0900
Subject: [PATCH 182/434] Fix SPM counter decreasing after spinner has already
 been completed

---
 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 69095fd160..e6940f0985 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -267,7 +267,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             if (!SpmCounter.IsPresent && RotationTracker.Tracking)
                 SpmCounter.FadeIn(HitObject.TimeFadeIn);
 
-            SpmCounter.SetRotation(Result.RateAdjustedRotation);
+            // don't update after end time to avoid the rate display dropping during fade out.
+            // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
+            if (Time.Current <= HitObject.EndTime)
+                SpmCounter.SetRotation(Result.RateAdjustedRotation);
 
             updateBonusScore();
         }

From 3f349816649689b519f3ed93942c311c3881a90e Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Thu, 11 Mar 2021 05:40:18 +0300
Subject: [PATCH 183/434] Fix incorrect spinner top offset calculation with
 clarification

---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 1738003390..ab7d265f67 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Skinning;
 using osuTK;
 
@@ -16,7 +17,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
     public abstract class LegacySpinner : CompositeDrawable
     {
-        protected static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE);
+        /// <remarks>
+        /// osu!stable applies this adjustment conditionally, locally in the spinner.
+        /// in lazer this is handled at a higher level in <see cref="OsuPlayfieldAdjustmentContainer"/>,
+        /// therefore it's safe to apply it unconditionally in this component.
+        /// </remarks>
+        protected static readonly float SPINNER_TOP_OFFSET = 45f - 16f;
+
         protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
 
         protected const float SPRITE_SCALE = 0.625f;

From efb4a366d42600b5217e574f40c5d53438a8d3c7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Mar 2021 12:15:59 +0900
Subject: [PATCH 184/434] Fix xmldoc explaining incorrect behaviour
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
---
 osu.Game/Storyboards/CommandTimelineGroup.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs
index 617455cf0b..c478b91c22 100644
--- a/osu.Game/Storyboards/CommandTimelineGroup.cs
+++ b/osu.Game/Storyboards/CommandTimelineGroup.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Storyboards
         }
 
         /// <summary>
-        /// Returns the earliest visible time. Will be null unless this group has an <see cref="Alpha"/> command with a start value of zero.
+        /// Returns the earliest visible time. Will be null unless this group's first <see cref="Alpha"/> command has a start value of zero.
         /// </summary>
         public double? EarliestDisplayedTime
         {

From 1591d593e26095201cedb9d2e1fde2f2761a09a6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Mar 2021 12:58:15 +0900
Subject: [PATCH 185/434] Move spin start time to inside result and switch to
 standard state handling

---
 .../Judgements/OsuSpinnerJudgementResult.cs   |  5 +++++
 .../Objects/Drawables/DrawableSpinner.cs      | 21 +++++++++++++++----
 .../Skinning/Default/SpinnerSpmCounter.cs     |  5 -----
 3 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
index e58aacd86e..9f77175398 100644
--- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs
@@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements
         /// </example>
         public float RateAdjustedRotation;
 
+        /// <summary>
+        /// Time instant at which the spin was started (the first user input which caused an increase in spin).
+        /// </summary>
+        public double? TimeStarted;
+
         /// <summary>
         /// Time instant at which the spinner has been completed (the user has executed all required spins).
         /// Will be null if all required spins haven't been completed.
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index e6940f0985..3d614c2dbd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -109,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             base.OnFree();
 
             spinningSample.Samples = null;
-
-            // the counter handles its own fade in (when spinning begins) so we should only be responsible for resetting it here, for pooling.
-            SpmCounter.Hide();
         }
 
         protected override void LoadSamples()
@@ -161,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             }
         }
 
+        protected override void UpdateStartTimeStateTransforms()
+        {
+            base.UpdateStartTimeStateTransforms();
+
+            if (Result?.TimeStarted is double startTime)
+            {
+                using (BeginAbsoluteSequence(startTime))
+                    fadeInCounter();
+            }
+        }
+
         protected override void UpdateHitStateTransforms(ArmedState state)
         {
             base.UpdateHitStateTransforms(state);
@@ -265,7 +273,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             base.UpdateAfterChildren();
 
             if (!SpmCounter.IsPresent && RotationTracker.Tracking)
-                SpmCounter.FadeIn(HitObject.TimeFadeIn);
+            {
+                Result.TimeStarted ??= Time.Current;
+                fadeInCounter();
+            }
 
             // don't update after end time to avoid the rate display dropping during fade out.
             // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
@@ -275,6 +286,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             updateBonusScore();
         }
 
+        private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
+
         private int wholeSpins;
 
         private void updateBonusScore()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
index f3e013c759..69355f624b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs
@@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
 
         private readonly OsuSpriteText spmText;
 
-        public override void ApplyTransformsAt(double time, bool propagateChildren = false)
-        {
-            // handles own fade in state.
-        }
-
         public SpinnerSpmCounter()
         {
             Children = new Drawable[]

From 8bc494b224639f79ab4bb6f0a31e18d89da5cfcf Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 11 Mar 2021 20:57:00 +0900
Subject: [PATCH 186/434] Adjust explanatory comments

---
 .../Skinning/Legacy/LegacySpinner.cs              | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index ab7d265f67..acaec9cbc0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -18,13 +18,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
     public abstract class LegacySpinner : CompositeDrawable
     {
         /// <remarks>
-        /// osu!stable applies this adjustment conditionally, locally in the spinner.
-        /// in lazer this is handled at a higher level in <see cref="OsuPlayfieldAdjustmentContainer"/>,
-        /// therefore it's safe to apply it unconditionally in this component.
+        /// All constant spinner coordinates are in osu!stable's gamefield space, which is shifted 16px downwards.
+        /// This offset is negated in both osu!stable and osu!lazer to bring all constant coordinates into window-space.
+        /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
         /// </remarks>
-        protected static readonly float SPINNER_TOP_OFFSET = 45f - 16f;
+        protected const float SPINNER_TOP_OFFSET = 45f - 16f;
 
-        protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
+        protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
 
         protected const float SPRITE_SCALE = 0.625f;
 
@@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
             Origin = Anchor.Centre;
             Size = new Vector2(640, 480);
 
-            // stable applies this adjustment conditionally, locally in the spinner.
-            // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer,
-            // therefore it's safe to apply it unconditionally in this component.
+            // osu!stable positions components of the spinner in window-space (as opposed to gamefield-space).
+            // in lazer, the gamefield-space transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to bring coordinates back into window-space.
             Position = new Vector2(0, -8f);
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;

From b5bdf235cad7a638ad0fc8ce62fd7f6a31edf094 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 11 Mar 2021 21:21:44 +0900
Subject: [PATCH 187/434] Slightly improve comments more

---
 .../Skinning/Legacy/LegacySpinner.cs                | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index acaec9cbc0..1cc25bf053 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
     public abstract class LegacySpinner : CompositeDrawable
     {
         /// <remarks>
-        /// All constant spinner coordinates are in osu!stable's gamefield space, which is shifted 16px downwards.
-        /// This offset is negated in both osu!stable and osu!lazer to bring all constant coordinates into window-space.
+        /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
+        /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
         /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
         /// </remarks>
         protected const float SPINNER_TOP_OFFSET = 45f - 16f;
@@ -36,15 +36,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         [BackgroundDependencyLoader]
         private void load(DrawableHitObject drawableHitObject, ISkinSource source)
         {
-            // legacy spinners relied heavily on absolute screen-space coordinate values.
-            // wrap everything in a container simulating absolute coords to preserve alignment
-            // as there are skins that depend on it.
             Anchor = Anchor.Centre;
             Origin = Anchor.Centre;
-            Size = new Vector2(640, 480);
 
-            // osu!stable positions components of the spinner in window-space (as opposed to gamefield-space).
-            // in lazer, the gamefield-space transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to bring coordinates back into window-space.
+            // osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen.
+            // In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space.
+            Size = new Vector2(640, 480);
             Position = new Vector2(0, -8f);
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;

From ea9b48d17d08444c2e7e458fd874cac580ec388c Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 11 Mar 2021 21:21:48 +0900
Subject: [PATCH 188/434] Remove unused using

---
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 1cc25bf053..513888db53 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Skinning;
 using osuTK;
 

From f1302d16006b567343c12a2624eeb544c7eae9bf Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Thu, 11 Mar 2021 19:23:56 +0300
Subject: [PATCH 189/434] Update Microsoft.EntityFrameworkCore

---
 osu.Desktop/osu.Desktop.csproj    | 7 +++++--
 osu.Game/Database/OsuDbContext.cs | 3 ---
 osu.Game/osu.Game.csproj          | 6 +++---
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 3e0f0cb7f6..4af69c573d 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,8 +27,11 @@
     <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
     <PackageReference Include="System.IO.Packaging" Version="5.0.0" />
     <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
     <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
   </ItemGroup>
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 2aae62edea..d27da50448 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -111,9 +111,6 @@ namespace osu.Game.Database
         {
             base.OnConfiguring(optionsBuilder);
             optionsBuilder
-                // this is required for the time being due to the way we are querying in places like BeatmapStore.
-                // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
-                .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
                 .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
                 .UseLoggerFactory(logger.Value);
         }
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 90c8b98f42..fa1b0a95c3 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,10 +24,10 @@
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.4" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
+    <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />

From 47b80d2474f6c2a371dd91aa48fda003a60e808e Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Thu, 11 Mar 2021 20:51:54 +0300
Subject: [PATCH 190/434] Workaround InvalidOperation exceptions

---
 osu.Game/Beatmaps/BeatmapManager.cs      | 16 ++++++++++++++++
 osu.Game/Database/ArchiveModelManager.cs |  8 +++++++-
 osu.Game/Skinning/SkinManager.cs         | 12 ++++++++++++
 3 files changed, 35 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 29b3f5d3a3..3254f53574 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -174,6 +174,22 @@ namespace osu.Game.Beatmaps
             if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
                 throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
 
+            var dbContext = ContextFactory.Get();
+
+            // Workaround System.InvalidOperationException
+            // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+            foreach (var beatmap in beatmapSet.Beatmaps)
+            {
+                beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID);
+            }
+
+            // Workaround System.InvalidOperationException
+            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+            foreach (var file in beatmapSet.Files)
+            {
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+
             // check if a set already exists with the same online id, delete if it does.
             if (beatmapSet.OnlineBeatmapSetID != null)
             {
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index d809dbcb01..fe2caaa0b7 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -462,6 +462,10 @@ namespace osu.Game.Database
                 // Dereference the existing file info, since the file model will be removed.
                 if (file.FileInfo != null)
                 {
+                    // Workaround System.InvalidOperationException
+                    // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+                    file.FileInfo = usage.Context.FileInfo.Find(file.FileInfoID);
+
                     Files.Dereference(file.FileInfo);
 
                     // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
@@ -635,10 +639,12 @@ namespace osu.Game.Database
             {
                 using (Stream s = reader.GetStream(file))
                 {
+                    var fileInfo = files.Add(s);
                     fileInfos.Add(new TFileModel
                     {
                         Filename = file.Substring(prefix.Length).ToStandardisedPath(),
-                        FileInfo = files.Add(s)
+                        FileInfo = fileInfo,
+                        FileInfoID = fileInfo.ID
                     });
                 }
             }
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index fcde9f041b..2bb27b60d6 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -142,6 +142,18 @@ namespace osu.Game.Skinning
             }
         }
 
+        protected override void PreImport(SkinInfo model)
+        {
+            var dbContext = ContextFactory.Get();
+
+            // Workaround System.InvalidOperationException
+            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+            foreach (var file in model.Files)
+            {
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+        }
+
         /// <summary>
         /// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
         /// </summary>

From c6c616f244eb08e664c04937234ffcd6dd84b9c0 Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Thu, 11 Mar 2021 21:02:40 +0300
Subject: [PATCH 191/434] Actualize tests

---
 .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs |  1 +
 .../SongSelect/TestSceneBeatmapRecommendations.cs      |  1 +
 .../Visual/SongSelect/TestScenePlaySongSelect.cs       |  4 +++-
 osu.Game/Scoring/ScoreInfo.cs                          | 10 ++--------
 4 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index faa5d9e6fc..4a9eaa1842 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                 beatmaps.Add(new BeatmapInfo
                 {
                     Ruleset = rulesets.GetRuleset(i % 4),
+                    RulesetID = i % 4,
                     OnlineBeatmapID = beatmapId,
                     Length = length,
                     BPM = bpm,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 53a956c77c..223ace6ca5 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -186,6 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Metadata = metadata,
                     BaseDifficulty = new BeatmapDifficulty(),
                     Ruleset = ruleset,
+                    RulesetID = ruleset.ID.GetValueOrDefault(),
                     StarDifficulty = difficultyIndex + 1,
                     Version = $"SR{difficultyIndex + 1}"
                 }).ToList()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 35c6d62cb7..4b402d0c54 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -911,9 +911,11 @@ namespace osu.Game.Tests.Visual.SongSelect
                 int length = RNG.Next(30000, 200000);
                 double bpm = RNG.NextSingle(80, 200);
 
+                var ruleset = getRuleset();
                 beatmaps.Add(new BeatmapInfo
                 {
-                    Ruleset = getRuleset(),
+                    Ruleset = ruleset,
+                    RulesetID = ruleset.ID.GetValueOrDefault(),
                     OnlineBeatmapID = beatmapId,
                     Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
                     Length = length,
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index f5192f3a40..c5ad43abba 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Scoring
             }
             set
             {
-                modsJson = null;
+                modsJson = JsonConvert.SerializeObject(value.Select(m => new DeserializedMod { Acronym = m.Acronym }));
                 mods = value;
             }
         }
@@ -88,13 +88,7 @@ namespace osu.Game.Scoring
         {
             get
             {
-                if (modsJson != null)
-                    return modsJson;
-
-                if (mods == null)
-                    return null;
-
-                return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
+                return modsJson;
             }
             set
             {

From d2f943395d349e08ae8e5a72b1f2996ea0d1d539 Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Thu, 11 Mar 2021 22:12:47 +0300
Subject: [PATCH 192/434] Hotfix importing scores from stable

---
 osu.Game/Scoring/ScoreManager.cs | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 96ec9644b5..a97c516a1b 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -52,6 +52,23 @@ namespace osu.Game.Scoring
             this.configManager = configManager;
         }
 
+        protected override void PreImport(ScoreInfo model)
+        {
+            var dbContext = ContextFactory.Get();
+
+            // Workaround System.InvalidOperationException
+            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+            foreach (var file in model.Files)
+            {
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+
+            foreach (var file in model.Beatmap.BeatmapSet.Files)
+            {
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+        }
+
         protected override ScoreInfo CreateModel(ArchiveReader archive)
         {
             if (archive == null)

From 5a4b0174b187e649b9fb739fafd076ea19817f23 Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Thu, 11 Mar 2021 22:40:35 +0300
Subject: [PATCH 193/434] Ignore MultipleCollectionIncludeWarning

---
 osu.Game/Database/OsuDbContext.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index d27da50448..689f248de8 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -112,6 +112,7 @@ namespace osu.Game.Database
             base.OnConfiguring(optionsBuilder);
             optionsBuilder
                 .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
+                .ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning))
                 .UseLoggerFactory(logger.Value);
         }
 

From a60ff80c04850c1f09ad9f741197378e691fc78d Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Fri, 12 Mar 2021 00:02:29 +0300
Subject: [PATCH 194/434] Use expression body in ModsJson get accessor

---
 osu.Game/Scoring/ScoreInfo.cs | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index c5ad43abba..78101991f6 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -86,10 +86,7 @@ namespace osu.Game.Scoring
         [Column("Mods")]
         public string ModsJson
         {
-            get
-            {
-                return modsJson;
-            }
+            get => modsJson;
             set
             {
                 modsJson = value;

From e7707eee94335149c2c284dfba421a93b11c0f69 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 15:23:11 +0900
Subject: [PATCH 195/434] Switch RestoreDefaultsValueButton to use
 HasPendingTasks to avoid tooltip always showing

---
 osu.Game/Overlays/Settings/SettingsItem.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs
index 8631b8ac7b..85765bf991 100644
--- a/osu.Game/Overlays/Settings/SettingsItem.cs
+++ b/osu.Game/Overlays/Settings/SettingsItem.cs
@@ -123,6 +123,8 @@ namespace osu.Game.Overlays.Settings
 
         protected internal class RestoreDefaultValueButton : Container, IHasTooltip
         {
+            public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
             private Bindable<T> bindable;
 
             public Bindable<T> Bindable
@@ -147,7 +149,6 @@ namespace osu.Game.Overlays.Settings
                 RelativeSizeAxes = Axes.Y;
                 Width = SettingsPanel.CONTENT_MARGINS;
                 Alpha = 0f;
-                AlwaysPresent = true;
             }
 
             [BackgroundDependencyLoader]

From b9b095ee75c960287a8636d06c07afddade6865f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Mar 2021 14:49:38 +0900
Subject: [PATCH 196/434] Local framework

---
 osu.Desktop.slnf         |  6 ++++--
 osu.Game/osu.Game.csproj |  4 +++-
 osu.iOS.props            |  4 ++--
 osu.sln                  | 42 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 51 insertions(+), 5 deletions(-)

diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
index d2c14d321a..1e41d0af0e 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -15,7 +15,9 @@
       "osu.Game.Tests\\osu.Game.Tests.csproj",
       "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
       "osu.Game.Tournament\\osu.Game.Tournament.csproj",
-      "osu.Game\\osu.Game.csproj"
+      "osu.Game\\osu.Game.csproj",
+      "../osu-framework/osu.Framework/osu.Framework.csproj",
+      "../osu-framework/osu.Framework/osu.Framework.NativeLibs.csproj"
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 90c8b98f42..f2fc1726cd 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,11 +29,13 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\osu-framework\osu.Framework\osu.Framework.csproj" />
+  </ItemGroup>
 </Project>
diff --git a/osu.iOS.props b/osu.iOS.props
index ccd33bf88c..30df8c423e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.309.0" />
+    <ProjectReference Include="../../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -93,7 +93,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
+    <ProjectReference Include="../../osu-framework/osu.Framework/osu.Framework.csproj" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />
diff --git a/osu.sln b/osu.sln
index c9453359b1..4d0b3656e7 100644
--- a/osu.sln
+++ b/osu.sln
@@ -66,6 +66,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Benchmarks", "osu.Game.Benchmarks\osu.Game.Benchmarks.csproj", "{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework", "..\osu-framework\osu.Framework\osu.Framework.csproj", "{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.iOS", "..\osu-framework\osu.Framework.iOS\osu.Framework.iOS.csproj", "{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.NativeLibs", "..\osu-framework\osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj", "{500039B3-0706-40C3-B6E7-1FD9187644A5}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -412,6 +418,42 @@ Global
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhone.Build.0 = Release|Any CPU
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhone.Build.0 = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhone.Build.0 = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhone.Build.0 = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

From a33ffd56b80878e02a0cf03cac93a22869af4787 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Mar 2021 12:43:04 +0900
Subject: [PATCH 197/434] Allow CreateSettingsControls to work with all
 bindables in target class

---
 .../Configuration/SettingSourceAttribute.cs   | 33 +++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index cfce615130..a8cebb97b4 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -7,6 +7,7 @@ using System.Linq;
 using System.Reflection;
 using JetBrains.Annotations;
 using osu.Framework.Bindables;
+using osu.Framework.Extensions.TypeExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Localisation;
 using osu.Game.Overlays.Settings;
@@ -61,9 +62,13 @@ namespace osu.Game.Configuration
 
     public static class SettingSourceExtensions
     {
-        public static IEnumerable<Drawable> CreateSettingsControls(this object obj)
+        public static IEnumerable<Drawable> CreateSettingsControls(this object obj) => createSettingsControls(obj, obj.GetOrderedSettingsSourceProperties());
+
+        public static IEnumerable<Drawable> CreateSettingsControlsFromAllBindables(this object obj) => createSettingsControls(obj, obj.GetSettingsSourcePropertiesFromBindables());
+
+        private static IEnumerable<Drawable> createSettingsControls(object obj, IEnumerable<(SettingSourceAttribute, PropertyInfo)> sourceAttribs)
         {
-            foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties())
+            foreach (var (attr, property) in sourceAttribs)
             {
                 object value = property.GetValue(obj);
 
@@ -139,6 +144,30 @@ namespace osu.Game.Configuration
             }
         }
 
+        public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourcePropertiesFromBindables(this object obj)
+        {
+            HashSet<string> handledProperties = new HashSet<string>();
+
+            // reverse and de-dupe properties to surface base class settings to the top of return order.
+            foreach (var type in obj.GetType().EnumerateBaseTypes().Reverse())
+            {
+                foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
+                {
+                    if (handledProperties.Contains(property.Name))
+                        continue;
+
+                    handledProperties.Add(property.Name);
+
+                    if (typeof(IBindable).IsAssignableFrom(property.PropertyType))
+                    {
+                        var val = property.GetValue(obj);
+                        string description = (val as IHasDescription)?.Description ?? string.Empty;
+                        yield return (new SettingSourceAttribute(property.Name, description), property);
+                    }
+                }
+            }
+        }
+
         public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
         {
             foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))

From 4374e7da81cbece7dfed94b3478e22b19b5b49ca Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 11 Mar 2021 15:26:18 +0900
Subject: [PATCH 198/434] Convert bindable names to human readable sentences

---
 osu.Game/Configuration/SettingSourceAttribute.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index a8cebb97b4..fe8886b52e 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
+using Humanizer;
 using JetBrains.Annotations;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.TypeExtensions;
@@ -162,7 +163,7 @@ namespace osu.Game.Configuration
                     {
                         var val = property.GetValue(obj);
                         string description = (val as IHasDescription)?.Description ?? string.Empty;
-                        yield return (new SettingSourceAttribute(property.Name, description), property);
+                        yield return (new SettingSourceAttribute(property.Name.Humanize(), description), property);
                     }
                 }
             }

From 6eadae8aaf56831d4ecfc19fb0f4dd1c0fdab2ee Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:35:42 +0900
Subject: [PATCH 199/434] Remove remnants of OsuTK

---
 osu.Desktop/OsuGameDesktop.cs | 22 +++++-----------------
 osu.Desktop/Program.cs        |  2 +-
 2 files changed, 6 insertions(+), 18 deletions(-)

diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 5909b82c8f..b2487568ce 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -136,24 +136,12 @@ namespace osu.Desktop
 
             var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
 
-            switch (host.Window)
-            {
-                // Legacy osuTK DesktopGameWindow
-                case OsuTKDesktopWindow desktopGameWindow:
-                    desktopGameWindow.CursorState |= CursorState.Hidden;
-                    desktopGameWindow.SetIconFromStream(iconStream);
-                    desktopGameWindow.Title = Name;
-                    desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
-                    break;
+            var desktopWindow = (SDL2DesktopWindow)host.Window;
 
-                // SDL2 DesktopWindow
-                case SDL2DesktopWindow desktopWindow:
-                    desktopWindow.CursorState |= CursorState.Hidden;
-                    desktopWindow.SetIconFromStream(iconStream);
-                    desktopWindow.Title = Name;
-                    desktopWindow.DragDrop += f => fileDrop(new[] { f });
-                    break;
-            }
+            desktopWindow.CursorState |= CursorState.Hidden;
+            desktopWindow.SetIconFromStream(iconStream);
+            desktopWindow.Title = Name;
+            desktopWindow.DragDrop += f => fileDrop(new[] { f });
         }
 
         private void fileDrop(string[] filePaths)
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 6ca7079654..0c527ba881 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -24,7 +24,7 @@ namespace osu.Desktop
             var cwd = Environment.CurrentDirectory;
             bool useOsuTK = args.Contains("--tk");
 
-            using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK))
+            using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
             {
                 host.ExceptionThrown += handleException;
 

From 3c21c83cc88e8a0613aeda4f6cb04069172c6777 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:36:28 +0900
Subject: [PATCH 200/434] Rename KeyboardSection to BindingSection

---
 .../Input/{KeyboardSettings.cs => BindingSettings.cs}     | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
 rename osu.Game/Overlays/Settings/Sections/Input/{KeyboardSettings.cs => BindingSettings.cs} (70%)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
similarity index 70%
rename from osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs
rename to osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
index db6f24a954..79c73863cf 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/BindingSettings.cs
@@ -5,17 +5,17 @@ using osu.Framework.Graphics;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
 {
-    public class KeyboardSettings : SettingsSubsection
+    public class BindingSettings : SettingsSubsection
     {
-        protected override string Header => "Keyboard";
+        protected override string Header => "Shortcut and gameplay bindings";
 
-        public KeyboardSettings(KeyBindingPanel keyConfig)
+        public BindingSettings(KeyBindingPanel keyConfig)
         {
             Children = new Drawable[]
             {
                 new SettingsButton
                 {
-                    Text = "Key configuration",
+                    Text = "Configure",
                     TooltipText = "change global shortcut keys and gameplay bindings",
                     Action = keyConfig.ToggleVisibility
                 },

From 8635abbc4a616771e7ed702140109737919a4dd5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:37:55 +0900
Subject: [PATCH 201/434] Add the ability to not get controls for disabled
 bindables

---
 osu.Game/Configuration/SettingSourceAttribute.cs | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index fe8886b52e..39d7fba32b 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -63,16 +63,21 @@ namespace osu.Game.Configuration
 
     public static class SettingSourceExtensions
     {
-        public static IEnumerable<Drawable> CreateSettingsControls(this object obj) => createSettingsControls(obj, obj.GetOrderedSettingsSourceProperties());
+        public static IReadOnlyList<Drawable> CreateSettingsControls(this object obj, bool includeDisabled = true) =>
+            createSettingsControls(obj, obj.GetOrderedSettingsSourceProperties(), includeDisabled).ToArray();
 
-        public static IEnumerable<Drawable> CreateSettingsControlsFromAllBindables(this object obj) => createSettingsControls(obj, obj.GetSettingsSourcePropertiesFromBindables());
+        public static IReadOnlyList<Drawable> CreateSettingsControlsFromAllBindables(this object obj, bool includeDisabled = true) =>
+            createSettingsControls(obj, obj.GetSettingsSourcePropertiesFromBindables(), includeDisabled).ToArray();
 
-        private static IEnumerable<Drawable> createSettingsControls(object obj, IEnumerable<(SettingSourceAttribute, PropertyInfo)> sourceAttribs)
+        private static IEnumerable<Drawable> createSettingsControls(object obj, IEnumerable<(SettingSourceAttribute, PropertyInfo)> sourceAttribs, bool includeDisabled = true)
         {
             foreach (var (attr, property) in sourceAttribs)
             {
                 object value = property.GetValue(obj);
 
+                if ((value as IBindable)?.Disabled == true)
+                    continue;
+
                 switch (value)
                 {
                     case BindableNumber<float> bNumber:

From 03230edcb11760ff457ce3667b3377e6aea30660 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:38:16 +0900
Subject: [PATCH 202/434] Update bindings settings to handle the new structure
 and show all handlers

---
 .../Settings/Sections/Input/MouseSettings.cs  | 60 ++++++-----------
 .../Settings/Sections/InputSection.cs         | 64 ++++++++++++++++++-
 2 files changed, 82 insertions(+), 42 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 3a78cff890..036c4edfba 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -1,11 +1,11 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Configuration;
 using osu.Framework.Graphics;
+using osu.Framework.Input.Handlers.Mouse;
 using osu.Game.Configuration;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Input;
@@ -14,35 +14,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 {
     public class MouseSettings : SettingsSubsection
     {
+        private readonly MouseHandler mouseHandler;
+
         protected override string Header => "Mouse";
 
-        private readonly BindableBool rawInputToggle = new BindableBool();
-
-        private Bindable<double> configSensitivity;
+        private Bindable<double> handlerSensitivity;
 
         private Bindable<double> localSensitivity;
 
-        private Bindable<string> ignoredInputHandlers;
-
         private Bindable<WindowMode> windowMode;
         private SettingsEnumDropdown<OsuConfineMouseMode> confineMouseModeSetting;
+        private Bindable<bool> relativeMode;
+
+        public MouseSettings(MouseHandler mouseHandler)
+        {
+            this.mouseHandler = mouseHandler;
+        }
 
         [BackgroundDependencyLoader]
         private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
         {
             // use local bindable to avoid changing enabled state of game host's bindable.
-            configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity);
-            localSensitivity = configSensitivity.GetUnboundCopy();
+            handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
+            localSensitivity = handlerSensitivity.GetUnboundCopy();
 
+            relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
             windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
-            ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
 
             Children = new Drawable[]
             {
                 new SettingsCheckbox
                 {
-                    LabelText = "Raw input",
-                    Current = rawInputToggle
+                    LabelText = "High precision mouse",
+                    Current = relativeMode
                 },
                 new SensitivitySetting
                 {
@@ -76,7 +80,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             base.LoadComplete();
 
-            configSensitivity.BindValueChanged(val =>
+            relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
+
+            handlerSensitivity.BindValueChanged(val =>
             {
                 var disabled = localSensitivity.Disabled;
 
@@ -85,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 localSensitivity.Disabled = disabled;
             }, true);
 
-            localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue);
+            localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
 
             windowMode.BindValueChanged(mode =>
             {
@@ -102,32 +108,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     confineMouseModeSetting.TooltipText = string.Empty;
                 }
             }, true);
-
-            if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
-            {
-                rawInputToggle.Disabled = true;
-                localSensitivity.Disabled = true;
-            }
-            else
-            {
-                rawInputToggle.ValueChanged += enabled =>
-                {
-                    // this is temporary until we support per-handler settings.
-                    const string raw_mouse_handler = @"OsuTKRawMouseHandler";
-                    const string standard_mouse_handlers = @"OsuTKMouseHandler MouseHandler";
-
-                    ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler;
-                };
-
-                ignoredInputHandlers.ValueChanged += handler =>
-                {
-                    bool raw = !handler.NewValue.Contains("Raw");
-                    rawInputToggle.Value = raw;
-                    localSensitivity.Disabled = !raw;
-                };
-
-                ignoredInputHandlers.TriggerChange();
-            }
         }
 
         private class SensitivitySetting : SettingsSlider<double, SensitivitySlider>
@@ -141,7 +121,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private class SensitivitySlider : OsuSliderBar<double>
         {
-            public override string TooltipText => Current.Disabled ? "enable raw input to adjust sensitivity" : $"{base.TooltipText}x";
+            public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
         }
     }
 }
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index b43453f53d..107e37909c 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -1,28 +1,88 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Handlers;
+using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Platform;
+using osu.Game.Configuration;
 using osu.Game.Overlays.Settings.Sections.Input;
 
 namespace osu.Game.Overlays.Settings.Sections
 {
     public class InputSection : SettingsSection
     {
+        private readonly KeyBindingPanel keyConfig;
+
         public override string Header => "Input";
 
+        [Resolved]
+        private GameHost host { get; set; }
+
         public override Drawable CreateIcon() => new SpriteIcon
         {
             Icon = FontAwesome.Solid.Keyboard
         };
 
         public InputSection(KeyBindingPanel keyConfig)
+        {
+            this.keyConfig = keyConfig;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
         {
             Children = new Drawable[]
             {
-                new MouseSettings(),
-                new KeyboardSettings(keyConfig),
+                new BindingSettings(keyConfig),
             };
+
+            foreach (var handler in host.AvailableInputHandlers)
+            {
+                var handlerSection = createSectionFor(handler);
+
+                if (handlerSection != null)
+                    Add(handlerSection);
+            }
+        }
+
+        private SettingsSubsection createSectionFor(InputHandler handler)
+        {
+            var settingsControls = handler.CreateSettingsControlsFromAllBindables(false);
+
+            if (settingsControls.Count == 0)
+                return null;
+
+            SettingsSubsection section;
+
+            switch (handler)
+            {
+                case MouseHandler mh:
+                    section = new MouseSettings(mh);
+                    break;
+
+                default:
+                    section = new HandlerSection(handler);
+                    break;
+            }
+
+            section.AddRange(settingsControls);
+
+            return section;
+        }
+
+        private class HandlerSection : SettingsSubsection
+        {
+            private readonly InputHandler handler;
+
+            public HandlerSection(InputHandler handler)
+            {
+                this.handler = handler;
+            }
+
+            protected override string Header => handler.Description;
         }
     }
 }

From 3458dcc33aa07615dad66ba2b2ee643f874b5f22 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:40:38 +0900
Subject: [PATCH 203/434] Use whitelist to avoid exposing settings to user that
 shouldn't be

---
 .../Settings/Sections/InputSection.cs         | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 107e37909c..e6aaa1ade9 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -5,6 +5,8 @@ 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.Platform;
 using osu.Game.Configuration;
@@ -50,11 +52,6 @@ namespace osu.Game.Overlays.Settings.Sections
 
         private SettingsSubsection createSectionFor(InputHandler handler)
         {
-            var settingsControls = handler.CreateSettingsControlsFromAllBindables(false);
-
-            if (settingsControls.Count == 0)
-                return null;
-
             SettingsSubsection section;
 
             switch (handler)
@@ -63,11 +60,21 @@ namespace osu.Game.Overlays.Settings.Sections
                     section = new MouseSettings(mh);
                     break;
 
-                default:
+                // whitelist the handlers which should be displayed to avoid any weird cases of users touching settings they shouldn't.
+                case JoystickHandler _:
+                case MidiHandler _:
                     section = new HandlerSection(handler);
                     break;
+
+                default:
+                    return null;
             }
 
+            var settingsControls = handler.CreateSettingsControlsFromAllBindables(false);
+
+            if (settingsControls.Count == 0)
+                return null;
+
             section.AddRange(settingsControls);
 
             return section;

From 86164c027a052af679caeb9d1937828bde61df8e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 12 Mar 2021 18:44:10 +0900
Subject: [PATCH 204/434] Update the method we use to reset input settings

---
 osu.Game/OsuGame.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index b7398efdc2..4dd7f97a72 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -880,8 +880,7 @@ namespace osu.Game
             switch (action)
             {
                 case GlobalAction.ResetInputSettings:
-                    frameworkConfig.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers).SetDefault();
-                    frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity).SetDefault();
+                    Host.ResetInputHandlers();
                     frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
                     return true;
 

From d0644221ff74801457b2b17efa728de723ddea20 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 9 Nov 2020 14:43:06 -0800
Subject: [PATCH 205/434] Add test showing toolbar behavior change

---
 .../Navigation/TestSceneScreenNavigation.cs   | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index fc49517cdf..f2bb518b2e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -229,6 +229,35 @@ namespace osu.Game.Tests.Visual.Navigation
             AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible);
         }
 
+        [Test]
+        public void TestToolbarHiddenByUser()
+        {
+            AddStep("Enter menu", () => InputManager.Key(Key.Enter));
+
+            AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded);
+
+            AddStep("Hide toolbar", () =>
+            {
+                InputManager.PressKey(Key.ControlLeft);
+                InputManager.Key(Key.T);
+                InputManager.ReleaseKey(Key.ControlLeft);
+            });
+
+            pushEscape();
+
+            AddStep("Enter menu", () => InputManager.Key(Key.Enter));
+
+            AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden);
+
+            AddStep("Enter song select", () =>
+            {
+                InputManager.Key(Key.Enter);
+                InputManager.Key(Key.Enter);
+            });
+
+            AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden);
+        }
+
         private void pushEscape() =>
             AddStep("Press escape", () => InputManager.Key(Key.Escape));
 

From 6c0734a09ff87754eb91917623f717476ee400e6 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 9 Nov 2020 14:45:20 -0800
Subject: [PATCH 206/434] Handle global action in toolbar instead of osugame

---
 osu.Game/OsuGame.cs                  |  4 ----
 osu.Game/Overlays/Toolbar/Toolbar.cs | 22 ++++++++++++++++++++--
 2 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index b7398efdc2..dd775888a1 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -885,10 +885,6 @@ namespace osu.Game
                     frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
                     return true;
 
-                case GlobalAction.ToggleToolbar:
-                    Toolbar.ToggleVisibility();
-                    return true;
-
                 case GlobalAction.ToggleGameplayMouseButtons:
                     LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get<bool>(OsuSetting.MouseDisableButtons));
                     return true;
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 0ccb22df3a..011f5a03c9 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -13,10 +13,12 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Input.Events;
 using osu.Game.Rulesets;
+using osu.Framework.Input.Bindings;
+using osu.Game.Input.Bindings;
 
 namespace osu.Game.Overlays.Toolbar
 {
-    public class Toolbar : VisibilityContainer
+    public class Toolbar : VisibilityContainer, IKeyBindingHandler<GlobalAction>
     {
         public const float HEIGHT = 40;
         public const float TOOLTIP_HEIGHT = 30;
@@ -30,7 +32,7 @@ namespace osu.Game.Overlays.Toolbar
 
         protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
 
-        // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden.
+        // Toolbar and its components need keyboard input even when hidden.
         public override bool PropagateNonPositionalInputSubTree => true;
 
         public Toolbar()
@@ -164,5 +166,21 @@ namespace osu.Game.Overlays.Toolbar
             this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint);
             this.FadeOut(transition_time, Easing.InQuint);
         }
+
+        public bool OnPressed(GlobalAction action)
+        {
+            switch (action)
+            {
+                case GlobalAction.ToggleToolbar:
+                    ToggleVisibility();
+                    return true;
+            }
+
+            return false;
+        }
+
+        public void OnReleased(GlobalAction action)
+        {
+        }
     }
 }

From 62f2a823f6be7d27ec7c6db751193023927bb130 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 9 Nov 2020 14:46:08 -0800
Subject: [PATCH 207/434] Hide toolbar forever when the user hides it

---
 osu.Game/OsuGame.cs                   | 2 +-
 osu.Game/Overlays/Toolbar/Toolbar.cs  | 6 ++++++
 osu.Game/Screens/Menu/ButtonSystem.cs | 3 ++-
 3 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index dd775888a1..fa9a0d4eb5 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -983,7 +983,7 @@ namespace osu.Game
 
                 if (newOsuScreen.HideOverlaysOnEnter)
                     CloseAllOverlays();
-                else
+                else if (!Toolbar.HiddenByUser)
                     Toolbar.Show();
 
                 if (newOsuScreen.AllowBackButton)
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 011f5a03c9..7f77e5add9 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Toolbar
         public const float HEIGHT = 40;
         public const float TOOLTIP_HEIGHT = 30;
 
+        public bool HiddenByUser;
+
         public Action OnHome;
 
         private ToolbarUserButton userButton;
@@ -169,10 +171,14 @@ namespace osu.Game.Overlays.Toolbar
 
         public bool OnPressed(GlobalAction action)
         {
+            if (OverlayActivationMode.Value == OverlayActivation.Disabled)
+                return false;
+
             switch (action)
             {
                 case GlobalAction.ToggleToolbar:
                     ToggleVisibility();
+                    HiddenByUser = State.Value == Visibility.Hidden;
                     return true;
             }
 
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 81b1cb0bf1..8f1fd627f5 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -352,7 +352,8 @@ namespace osu.Game.Screens.Menu
                                 if (impact)
                                     logo.Impact();
 
-                                game?.Toolbar.Show();
+                                if (game?.Toolbar.HiddenByUser == false)
+                                    game.Toolbar.Show();
                             }, 200);
                             break;
 

From 020a03e01ee7284d37359a67704b3e72ed8ff50a Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sat, 13 Mar 2021 05:22:20 +0300
Subject: [PATCH 208/434] Use sensible "score per tick" constant

---
 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 8534cd89d7..1ec3c877e9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -289,6 +289,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
 
         private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
 
+        private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
+
         private int wholeSpins;
 
         private void updateBonusScore()
@@ -315,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                     tick.TriggerResult(true);
 
                     if (tick is DrawableSpinnerBonusTick)
-                        gainedBonus.Value = tick.Result.Judgement.MaxNumericResult * (spins - HitObject.SpinsRequired);
+                        gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired);
                 }
 
                 wholeSpins++;

From 8fdab5a7de4a881604ebb5da7547106a153bf9dc Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 12 Mar 2021 02:37:07 +0300
Subject: [PATCH 209/434] Revert legacy spinner presence changes and bonus
 counter component

No longer necessary, after inlining legacy coordinates logic to `LegacySpinner` and limiting precisely-positioned legacy components there
---
 .../Objects/Drawables/DrawableSpinner.cs      |  1 -
 osu.Game.Rulesets.Osu/OsuSkinComponents.cs    |  1 -
 .../Legacy/OsuLegacySkinTransformer.cs        | 39 ++-----------------
 3 files changed, 4 insertions(+), 37 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 1ec3c877e9..a4919d5061 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -82,7 +82,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                     Y = 120,
                     Alpha = 0
                 },
-                new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBonusCounter), _ => new DefaultSpinnerBonusCounter()),
                 spinningSample = new PausableSkinnableSound
                 {
                     Volume = { Value = 0 },
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 131645406e..fcb544fa5b 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -19,6 +19,5 @@ namespace osu.Game.Rulesets.Osu
         SliderBall,
         SliderBody,
         SpinnerBody,
-        SpinnerBonusCounter,
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index ed09031fc1..d74f885573 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Game.Skinning;
 using osuTK;
-using static osu.Game.Skinning.LegacySkinConfiguration;
 
 namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
@@ -14,12 +13,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
     {
         private Lazy<bool> hasHitCircle;
 
-        private Lazy<SpinnerStyle> spinnerStyle;
-
-        private bool hasSpinner => spinnerStyle.Value != SpinnerStyle.Modern;
-
-        private bool hasScoreFont => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ScorePrefix)?.Value ?? "score");
-
         /// <summary>
         /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
         /// Their hittable area is 128px, but the actual circle portion is 118px.
@@ -37,19 +30,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         private void sourceChanged()
         {
             hasHitCircle = new Lazy<bool>(() => Source.GetTexture("hitcircle") != null);
-
-            spinnerStyle = new Lazy<SpinnerStyle>(() =>
-            {
-                bool hasBackground = Source.GetTexture("spinner-background") != null;
-
-                if (Source.GetTexture("spinner-top") != null && !hasBackground)
-                    return SpinnerStyle.NewLegacy;
-
-                if (hasBackground)
-                    return SpinnerStyle.OldLegacy;
-
-                return SpinnerStyle.Modern;
-            });
         }
 
         public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -130,18 +110,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         };
 
                 case OsuSkinComponents.SpinnerBody:
-                    if (spinnerStyle.Value == SpinnerStyle.NewLegacy)
+                    bool hasBackground = Source.GetTexture("spinner-background") != null;
+
+                    if (Source.GetTexture("spinner-top") != null && !hasBackground)
                         return new LegacyNewStyleSpinner();
-                    else if (spinnerStyle.Value == SpinnerStyle.OldLegacy)
+                    else if (hasBackground)
                         return new LegacyOldStyleSpinner();
 
                     return null;
-
-                case OsuSkinComponents.SpinnerBonusCounter:
-                    if (hasSpinner && hasScoreFont)
-                        return new LegacySpinnerBonusCounter();
-
-                    return null;
             }
 
             return null;
@@ -175,12 +151,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             return Source.GetConfig<TLookup, TValue>(lookup);
         }
-
-        private enum SpinnerStyle
-        {
-            NewLegacy,
-            OldLegacy,
-            Modern,
-        }
     }
 }

From 774ebf50bca5d5c0022c824db4e0d5d50e60281b Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 12 Mar 2021 02:38:25 +0300
Subject: [PATCH 210/434] Move legacy spinner bonus counter to `LegacySpinner`

---
 .../Skinning/Legacy/LegacySpinner.cs          | 62 +++++++++++++------
 .../Legacy/LegacySpinnerBonusCounter.cs       | 56 -----------------
 2 files changed, 43 insertions(+), 75 deletions(-)
 delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 513888db53..b0b9cba2bd 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         private Sprite spin;
         private Sprite clear;
 
+        private LegacySpriteText bonusCounter;
+
         [BackgroundDependencyLoader]
         private void load(DrawableHitObject drawableHitObject, ISkinSource source)
         {
@@ -45,36 +47,58 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            AddRangeInternal(new[]
+            AddInternal(new Container
             {
-                spin = new Sprite
+                Depth = float.MinValue,
+                RelativeSizeAxes = Axes.Both,
+                Children = new Drawable[]
                 {
-                    Anchor = Anchor.TopCentre,
-                    Origin = Anchor.Centre,
-                    Depth = float.MinValue,
-                    Texture = source.GetTexture("spinner-spin"),
-                    Scale = new Vector2(SPRITE_SCALE),
-                    Y = SPINNER_TOP_OFFSET + 335,
-                },
-                clear = new Sprite
-                {
-                    Alpha = 0,
-                    Anchor = Anchor.TopCentre,
-                    Origin = Anchor.Centre,
-                    Depth = float.MinValue,
-                    Texture = source.GetTexture("spinner-clear"),
-                    Scale = new Vector2(SPRITE_SCALE),
-                    Y = SPINNER_TOP_OFFSET + 115,
-                },
+                    spin = new Sprite
+                    {
+                        Anchor = Anchor.TopCentre,
+                        Origin = Anchor.Centre,
+                        Texture = source.GetTexture("spinner-spin"),
+                        Scale = new Vector2(SPRITE_SCALE),
+                        Y = SPINNER_TOP_OFFSET + 335,
+                    },
+                    clear = new Sprite
+                    {
+                        Alpha = 0,
+                        Anchor = Anchor.TopCentre,
+                        Origin = Anchor.Centre,
+                        Texture = source.GetTexture("spinner-clear"),
+                        Scale = new Vector2(SPRITE_SCALE),
+                        Y = SPINNER_TOP_OFFSET + 115,
+                    },
+                    bonusCounter = ((LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s =>
+                    {
+                        s.Alpha = 0f;
+                        s.Anchor = Anchor.TopCentre;
+                        s.Origin = Anchor.Centre;
+                        s.Font = s.Font.With(fixedWidth: false);
+                        s.Scale = new Vector2(SPRITE_SCALE);
+                        s.Y = SPINNER_TOP_OFFSET + 299;
+                    }),
+                }
             });
         }
 
+        private IBindable<double> gainedBonus;
+
         private readonly Bindable<bool> completed = new Bindable<bool>();
 
         protected override void LoadComplete()
         {
             base.LoadComplete();
 
+            gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
+            gainedBonus.BindValueChanged(bonus =>
+            {
+                bonusCounter.Text = $"{bonus.NewValue}";
+                bonusCounter.FadeOutFromOne(800, Easing.Out);
+                bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
+            });
+
             completed.BindValueChanged(onCompletedChanged, true);
 
             DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs
deleted file mode 100644
index 3c4a6be4dd..0000000000
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinnerBonusCounter.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Skinning;
-using osuTK;
-using static osu.Game.Rulesets.Osu.Skinning.Legacy.LegacySpinner;
-
-namespace osu.Game.Rulesets.Osu.Skinning.Legacy
-{
-    public class LegacySpinnerBonusCounter : CompositeDrawable
-    {
-        private LegacySpriteText bonusCounter;
-
-        private DrawableSpinner drawableSpinner;
-
-        private IBindable<double> gainedBonus;
-
-        [BackgroundDependencyLoader]
-        private void load(DrawableHitObject drawableHitObject, ISkinSource source)
-        {
-            drawableSpinner = (DrawableSpinner)drawableHitObject;
-
-            InternalChild = new LegacyCoordinatesContainer
-            {
-                Child = bonusCounter = ((LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s =>
-                {
-                    s.Alpha = 0f;
-                    s.Anchor = Anchor.TopCentre;
-                    s.Origin = Anchor.Centre;
-                    s.Font = s.Font.With(fixedWidth: false);
-                    s.Scale = new Vector2(SPRITE_SCALE);
-                    s.Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 299;
-                }),
-            };
-        }
-
-        protected override void LoadComplete()
-        {
-            base.LoadComplete();
-
-            gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
-            gainedBonus.BindValueChanged(bonus =>
-            {
-                bonusCounter.Text = $"{bonus.NewValue}";
-                bonusCounter.FadeOutFromOne(800, Easing.Out);
-                bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
-            });
-        }
-    }
-}

From 98f6e16113debd5a4890d7e6eb6aad4a70226bad Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 12 Mar 2021 02:38:40 +0300
Subject: [PATCH 211/434] Move default spinner bonus counter to new
 `DefaultSpinner`

---
 .../Objects/Drawables/DrawableSpinner.cs      |  2 +-
 ...innerBonusCounter.cs => DefaultSpinner.cs} | 38 +++++++++++++------
 .../Skinning/Default/DefaultSpinnerDisc.cs    |  5 ---
 3 files changed, 28 insertions(+), 17 deletions(-)
 rename osu.Game.Rulesets.Osu/Skinning/Default/{DefaultSpinnerBonusCounter.cs => DefaultSpinner.cs} (62%)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index a4919d5061..f995140123 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
                     RelativeSizeAxes = Axes.Y,
                     Children = new Drawable[]
                     {
-                        new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
+                        new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
                         RotationTracker = new SpinnerRotationTracker(this)
                     }
                 },
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
similarity index 62%
rename from osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs
rename to osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
index 633766290f..83676d3784 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerBonusCounter.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
@@ -12,29 +12,45 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
 
 namespace osu.Game.Rulesets.Osu.Skinning.Default
 {
-    public class DefaultSpinnerBonusCounter : CompositeDrawable
+    public class DefaultSpinner : CompositeDrawable
     {
-        private OsuSpriteText bonusCounter;
-
         private DrawableSpinner drawableSpinner;
 
-        private IBindable<double> gainedBonus;
+        private OsuSpriteText bonusCounter;
+
+        public DefaultSpinner()
+        {
+            RelativeSizeAxes = Axes.Both;
+            Anchor = Anchor.Centre;
+            Origin = Anchor.Centre;
+        }
 
         [BackgroundDependencyLoader]
         private void load(DrawableHitObject drawableHitObject)
         {
             drawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            InternalChild = bonusCounter = new OsuSpriteText
+            AddRangeInternal(new Drawable[]
             {
-                Alpha = 0,
-                Anchor = Anchor.Centre,
-                Origin = Anchor.Centre,
-                Font = OsuFont.Numeric.With(size: 24),
-                Y = -120,
-            };
+                new DefaultSpinnerDisc
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                },
+                bonusCounter = new OsuSpriteText
+                {
+                    Alpha = 0,
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Font = OsuFont.Numeric.With(size: 24),
+                    Y = -120,
+                }
+            });
         }
 
+        private IBindable<double> gainedBonus;
+
         protected override void LoadComplete()
         {
             base.LoadComplete();
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
index 667fee1495..542f3eff0d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
@@ -40,14 +40,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
 
         public DefaultSpinnerDisc()
         {
-            RelativeSizeAxes = Axes.Both;
-
             // we are slightly bigger than our parent, to clip the top and bottom of the circle
             // this should probably be revisited when scaled spinners are a thing.
             Scale = new Vector2(initial_scale);
-
-            Anchor = Anchor.Centre;
-            Origin = Anchor.Centre;
         }
 
         [BackgroundDependencyLoader]

From 115c186cb7a624065620391a83be90c502827bb2 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 02:16:10 +0300
Subject: [PATCH 212/434] Move hit circle font from osu! ruleset

---
 osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 2 --
 osu.Game/Skinning/LegacySkinConfiguration.cs           | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 63c9b53278..75a62a6f8e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -5,8 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
 {
     public enum OsuSkinConfiguration
     {
-        HitCirclePrefix,
-        HitCircleOverlap,
         SliderBorderSize,
         SliderPathRadius,
         AllowSliderBallTint,
diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs
index 84a834ec22..20d1da8aaa 100644
--- a/osu.Game/Skinning/LegacySkinConfiguration.cs
+++ b/osu.Game/Skinning/LegacySkinConfiguration.cs
@@ -19,6 +19,8 @@ namespace osu.Game.Skinning
             ComboOverlap,
             ScorePrefix,
             ScoreOverlap,
+            HitCirclePrefix,
+            HitCircleOverlap,
             AnimationFramerate,
             LayeredHitSounds
         }

From 91741564e8e06926a36b49f1c15dba97a03564da Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 02:15:23 +0300
Subject: [PATCH 213/434] Add legacy font enum and extensions

---
 osu.Game/Skinning/LegacyFont.cs           | 15 +++++++
 osu.Game/Skinning/LegacySkinExtensions.cs | 48 ++++++++++++++++++++++-
 2 files changed, 61 insertions(+), 2 deletions(-)
 create mode 100644 osu.Game/Skinning/LegacyFont.cs

diff --git a/osu.Game/Skinning/LegacyFont.cs b/osu.Game/Skinning/LegacyFont.cs
new file mode 100644
index 0000000000..d1971cb84c
--- /dev/null
+++ b/osu.Game/Skinning/LegacyFont.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Skinning
+{
+    /// <summary>
+    /// The type of legacy font to use for <see cref="LegacySpriteText"/>s.
+    /// </summary>
+    public enum LegacyFont
+    {
+        Score,
+        Combo,
+        HitCircle,
+    }
+}
diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index a7c084998d..d08f50bccb 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
@@ -63,8 +64,51 @@ namespace osu.Game.Skinning
             }
         }
 
-        public static bool HasFont(this ISkin source, string fontPrefix)
-            => source.GetTexture($"{fontPrefix}-0") != null;
+        public static bool HasFont(this ISkin source, LegacyFont font)
+        {
+            return source.GetTexture($"{source.GetFontPrefix(font)}-0") != null;
+        }
+
+        public static string GetFontPrefix(this ISkin source, LegacyFont font)
+        {
+            switch (font)
+            {
+                case LegacyFont.Score:
+                    return source.GetConfig<LegacySetting, string>(LegacySetting.ScorePrefix)?.Value ?? "score";
+
+                case LegacyFont.Combo:
+                    return source.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
+
+                case LegacyFont.HitCircle:
+                    return source.GetConfig<LegacySetting, string>(LegacySetting.HitCirclePrefix)?.Value ?? "default";
+
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(font));
+            }
+        }
+
+        /// <summary>
+        /// Returns the numeric overlap of number sprites to use.
+        /// A positive number will bring the number sprites closer together, while a negative number
+        /// will split them apart more.
+        /// </summary>
+        public static float GetFontOverlap(this ISkin source, LegacyFont font)
+        {
+            switch (font)
+            {
+                case LegacyFont.Score:
+                    return source.GetConfig<LegacySetting, float>(LegacySetting.ScoreOverlap)?.Value ?? -2f;
+
+                case LegacyFont.Combo:
+                    return source.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
+
+                case LegacyFont.HitCircle:
+                    return source.GetConfig<LegacySetting, float>(LegacySetting.HitCircleOverlap)?.Value ?? -2f;
+
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(font));
+            }
+        }
 
         public class SkinnableTextureAnimation : TextureAnimation
         {

From 64d1cb519324df2663aa85502ba6bed305ae1b0f Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 02:30:16 +0300
Subject: [PATCH 214/434] Remove text skin components in favour of plain
 `LegacySpriteText`s

---
 osu.Game/Skinning/HUDSkinComponents.cs |  2 --
 osu.Game/Skinning/LegacySkin.cs        | 21 +--------------------
 2 files changed, 1 insertion(+), 22 deletions(-)

diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs
index b01be2d5a0..a345e060e5 100644
--- a/osu.Game/Skinning/HUDSkinComponents.cs
+++ b/osu.Game/Skinning/HUDSkinComponents.cs
@@ -9,7 +9,5 @@ namespace osu.Game.Skinning
         ScoreCounter,
         AccuracyCounter,
         HealthDisplay,
-        ScoreText,
-        ComboText,
     }
 }
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index e5d0217671..83854c592b 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -18,7 +18,6 @@ using osu.Game.Beatmaps.Formats;
 using osu.Game.IO;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Screens.Play.HUD;
-using osuTK;
 using osuTK.Graphics;
 
 namespace osu.Game.Skinning
@@ -327,19 +326,13 @@ namespace osu.Game.Skinning
             return null;
         }
 
-        private string scorePrefix => GetConfig<LegacySkinConfiguration.LegacySetting, string>(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score";
-
-        private string comboPrefix => GetConfig<LegacySkinConfiguration.LegacySetting, string>(LegacySkinConfiguration.LegacySetting.ComboPrefix)?.Value ?? "score";
-
-        private bool hasScoreFont => this.HasFont(scorePrefix);
-
         public override Drawable GetDrawableComponent(ISkinComponent component)
         {
             switch (component)
             {
                 case HUDSkinComponent hudComponent:
                 {
-                    if (!hasScoreFont)
+                    if (!this.HasFont(LegacyFont.Score))
                         return null;
 
                     switch (hudComponent.Component)
@@ -355,18 +348,6 @@ namespace osu.Game.Skinning
 
                         case HUDSkinComponents.HealthDisplay:
                             return new LegacyHealthDisplay(this);
-
-                        case HUDSkinComponents.ComboText:
-                            return new LegacySpriteText(this, comboPrefix)
-                            {
-                                Spacing = new Vector2(-(GetConfig<LegacySkinConfiguration.LegacySetting, int>(LegacySkinConfiguration.LegacySetting.ComboOverlap)?.Value ?? -2), 0)
-                            };
-
-                        case HUDSkinComponents.ScoreText:
-                            return new LegacySpriteText(this, scorePrefix)
-                            {
-                                Spacing = new Vector2(-(GetConfig<LegacySkinConfiguration.LegacySetting, int>(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2), 0)
-                            };
                     }
 
                     return null;

From 2a2ee3fa5ea86bbfc875f5659c01b3463adb93fb Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 02:28:08 +0300
Subject: [PATCH 215/434] Update legacy sprite text constructor

---
 osu.Game/Skinning/LegacyRollingCounter.cs | 23 +++++------------------
 osu.Game/Skinning/LegacySpriteText.cs     |  7 +++++--
 2 files changed, 10 insertions(+), 20 deletions(-)

diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs
index 8aa9d4e9af..0261db0e64 100644
--- a/osu.Game/Skinning/LegacyRollingCounter.cs
+++ b/osu.Game/Skinning/LegacyRollingCounter.cs
@@ -4,7 +4,6 @@
 using System;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
-using osuTK;
 
 namespace osu.Game.Skinning
 {
@@ -14,9 +13,7 @@ namespace osu.Game.Skinning
     public class LegacyRollingCounter : RollingCounter<int>
     {
         private readonly ISkin skin;
-
-        private readonly string fontName;
-        private readonly float fontOverlap;
+        private readonly LegacyFont font;
 
         protected override bool IsRollingProportional => true;
 
@@ -24,17 +21,11 @@ namespace osu.Game.Skinning
         /// Creates a new <see cref="LegacyRollingCounter"/>.
         /// </summary>
         /// <param name="skin">The <see cref="ISkin"/> from which to get counter number sprites.</param>
-        /// <param name="fontName">The name of the legacy font to use.</param>
-        /// <param name="fontOverlap">
-        /// The numeric overlap of number sprites to use.
-        /// A positive number will bring the number sprites closer together, while a negative number
-        /// will split them apart more.
-        /// </param>
-        public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap)
+        /// <param name="font">The legacy font to use for the counter.</param>
+        public LegacyRollingCounter(ISkin skin, LegacyFont font)
         {
             this.skin = skin;
-            this.fontName = fontName;
-            this.fontOverlap = fontOverlap;
+            this.font = font;
         }
 
         protected override double GetProportionalDuration(int currentValue, int newValue)
@@ -42,10 +33,6 @@ namespace osu.Game.Skinning
             return Math.Abs(newValue - currentValue) * 75.0;
         }
 
-        protected sealed override OsuSpriteText CreateSpriteText() =>
-            new LegacySpriteText(skin, fontName)
-            {
-                Spacing = new Vector2(-fontOverlap, 0f)
-            };
+        protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, font);
     }
 }
diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs
index 5d0e312f7c..c55400e219 100644
--- a/osu.Game/Skinning/LegacySpriteText.cs
+++ b/osu.Game/Skinning/LegacySpriteText.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Text;
 using osu.Game.Graphics.Sprites;
+using osuTK;
 
 namespace osu.Game.Skinning
 {
@@ -16,12 +17,14 @@ namespace osu.Game.Skinning
 
         protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' };
 
-        public LegacySpriteText(ISkin skin, string font = "score")
+        public LegacySpriteText(ISkin skin, LegacyFont font)
         {
             Shadow = false;
             UseFullGlyphHeight = false;
 
-            Font = new FontUsage(font, 1, fixedWidth: true);
+            Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true);
+            Spacing = new Vector2(-skin.GetFontOverlap(font), 0);
+
             glyphStore = new LegacyGlyphStore(skin);
         }
 

From 43c1e1d217388f56fe5e1e616fb000c65f7d2382 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sun, 7 Mar 2021 02:18:31 +0300
Subject: [PATCH 216/434] Update existing usages

Resolve post-conflict issues
---
 .../Legacy/CatchLegacySkinTransformer.cs      |  4 +---
 .../Legacy/LegacyCatchComboCounter.cs         |  8 ++------
 .../Legacy/OsuLegacySkinTransformer.cs        | 17 +++++++----------
 .../Screens/Play/HUD/LegacyComboCounter.cs    | 19 ++++++++-----------
 osu.Game/Skinning/LegacyAccuracyCounter.cs    |  8 +++++---
 osu.Game/Skinning/LegacyScoreCounter.cs       |  8 +++++---
 6 files changed, 28 insertions(+), 36 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 41fd0fe776..1b48832ed6 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -5,7 +5,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Game.Skinning;
 using osuTK.Graphics;
-using static osu.Game.Skinning.LegacySkinConfiguration;
 
 namespace osu.Game.Rulesets.Catch.Skinning.Legacy
 {
@@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
         /// <summary>
         /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
         /// </summary>
-        private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
+        private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
 
         public CatchLegacySkinTransformer(ISkinSource source)
             : base(source)
@@ -69,7 +68,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
                            this.GetAnimation("fruit-ryuuta", true, true, true);
 
                 case CatchSkinComponents.CatchComboCounter:
-
                     if (providesComboCounter)
                         return new LegacyCatchComboCounter(Source);
 
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
index f797ae75c2..28ee7bd813 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs
@@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.UI;
 using osu.Game.Skinning;
 using osuTK;
 using osuTK.Graphics;
-using static osu.Game.Skinning.LegacySkinConfiguration;
 
 namespace osu.Game.Rulesets.Catch.Skinning.Legacy
 {
@@ -22,9 +21,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
 
         public LegacyCatchComboCounter(ISkin skin)
         {
-            var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
-            var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
-
             AutoSizeAxes = Axes.Both;
 
             Alpha = 0f;
@@ -34,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
 
             InternalChildren = new Drawable[]
             {
-                explosion = new LegacyRollingCounter(skin, fontName, fontOverlap)
+                explosion = new LegacyRollingCounter(skin, LegacyFont.Combo)
                 {
                     Alpha = 0.65f,
                     Blending = BlendingParameters.Additive,
@@ -42,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
                     Origin = Anchor.Centre,
                     Scale = new Vector2(1.5f),
                 },
-                counter = new LegacyRollingCounter(skin, fontName, fontOverlap)
+                counter = new LegacyRollingCounter(skin, LegacyFont.Combo)
                 {
                     Anchor = Anchor.Centre,
                     Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index d74f885573..ffe238c507 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -97,17 +97,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                     return null;
 
                 case OsuSkinComponents.HitCircleText:
-                    var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
-                    var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
+                    if (!this.HasFont(LegacyFont.HitCircle))
+                        return null;
 
-                    return !this.HasFont(font)
-                        ? null
-                        : new LegacySpriteText(Source, font)
-                        {
-                            // stable applies a blanket 0.8x scale to hitcircle fonts
-                            Scale = new Vector2(0.8f),
-                            Spacing = new Vector2(-overlap, 0)
-                        };
+                    return new LegacySpriteText(Source, LegacyFont.HitCircle)
+                    {
+                        // stable applies a blanket 0.8x scale to hitcircle fonts
+                        Scale = new Vector2(0.8f),
+                    };
 
                 case OsuSkinComponents.SpinnerBody:
                     bool hasBackground = Source.GetTexture("spinner-background") != null;
diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
index 81183a425a..b4604c0d01 100644
--- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
+++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics.Sprites;
 using osu.Game.Skinning;
 using osuTK;
 
@@ -84,16 +83,16 @@ namespace osu.Game.Screens.Play.HUD
         {
             InternalChildren = new[]
             {
-                popOutCount = createSpriteText().With(s =>
+                popOutCount = new LegacySpriteText(skin, LegacyFont.Combo)
                 {
-                    s.Alpha = 0;
-                    s.Margin = new MarginPadding(0.05f);
-                    s.Blending = BlendingParameters.Additive;
-                }),
-                displayedCountSpriteText = createSpriteText().With(s =>
+                    Alpha = 0,
+                    Margin = new MarginPadding(0.05f),
+                    Blending = BlendingParameters.Additive,
+                },
+                displayedCountSpriteText = new LegacySpriteText(skin, LegacyFont.Combo)
                 {
-                    s.Alpha = 0;
-                })
+                    Alpha = 0,
+                },
             };
 
             Current.ValueChanged += combo => updateCount(combo.NewValue == 0);
@@ -247,7 +246,5 @@ namespace osu.Game.Screens.Play.HUD
             double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
             return difference * rolling_duration;
         }
-
-        private OsuSpriteText createSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboText));
     }
 }
diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs
index 5eda374337..7d6f1dc916 100644
--- a/osu.Game/Skinning/LegacyAccuracyCounter.cs
+++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs
@@ -29,9 +29,11 @@ namespace osu.Game.Skinning
         [Resolved(canBeNull: true)]
         private HUDOverlay hud { get; set; }
 
-        protected sealed override OsuSpriteText CreateSpriteText()
-            => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))
-                                  ?.With(s => s.Anchor = s.Origin = Anchor.TopRight);
+        protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score)
+        {
+            Anchor = Anchor.TopRight,
+            Origin = Anchor.TopRight,
+        };
 
         protected override void Update()
         {
diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs
index 5bffeff5a8..1d330ef495 100644
--- a/osu.Game/Skinning/LegacyScoreCounter.cs
+++ b/osu.Game/Skinning/LegacyScoreCounter.cs
@@ -33,8 +33,10 @@ namespace osu.Game.Skinning
             Margin = new MarginPadding(10);
         }
 
-        protected sealed override OsuSpriteText CreateSpriteText()
-            => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))
-                                  .With(s => s.Anchor = s.Origin = Anchor.TopRight);
+        protected sealed override OsuSpriteText CreateSpriteText() => new LegacySpriteText(skin, LegacyFont.Score)
+        {
+            Anchor = Anchor.TopRight,
+            Origin = Anchor.TopRight,
+        };
     }
 }

From 5999e4ba3361f54828eaa25f1539ca0e2b991279 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 9 Nov 2020 15:16:35 -0800
Subject: [PATCH 217/434] Add xmldoc for hiddenbyuser bool

---
 osu.Game/Overlays/Toolbar/Toolbar.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 7f77e5add9..483d82200b 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -23,6 +23,9 @@ namespace osu.Game.Overlays.Toolbar
         public const float HEIGHT = 40;
         public const float TOOLTIP_HEIGHT = 30;
 
+        /// <summary>
+        /// Whether the user hid this <see cref="Toolbar"/> with <see cref="GlobalAction.ToggleToolbar"/>.
+        /// </summary>
         public bool HiddenByUser;
 
         public Action OnHome;

From 0ba5312a4063e3308c666e17ab4590bfe2f38267 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Sat, 13 Mar 2021 00:05:26 -0800
Subject: [PATCH 218/434] Move blocking show logic to UpdateState

---
 osu.Game/OsuGame.cs                   |  2 +-
 osu.Game/Overlays/Toolbar/Toolbar.cs  | 12 +++++++++---
 osu.Game/Screens/Menu/ButtonSystem.cs |  3 +--
 3 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index fa9a0d4eb5..dd775888a1 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -983,7 +983,7 @@ namespace osu.Game
 
                 if (newOsuScreen.HideOverlaysOnEnter)
                     CloseAllOverlays();
-                else if (!Toolbar.HiddenByUser)
+                else
                     Toolbar.Show();
 
                 if (newOsuScreen.AllowBackButton)
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 483d82200b..7497f4d210 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -26,7 +26,9 @@ namespace osu.Game.Overlays.Toolbar
         /// <summary>
         /// Whether the user hid this <see cref="Toolbar"/> with <see cref="GlobalAction.ToggleToolbar"/>.
         /// </summary>
-        public bool HiddenByUser;
+        private bool hiddenByUser;
+
+        private bool userToggled;
 
         public Action OnHome;
 
@@ -149,7 +151,9 @@ namespace osu.Game.Overlays.Toolbar
 
         protected override void UpdateState(ValueChangedEvent<Visibility> state)
         {
-            if (state.NewValue == Visibility.Visible && OverlayActivationMode.Value == OverlayActivation.Disabled)
+            var blockShow = !userToggled && hiddenByUser;
+
+            if (state.NewValue == Visibility.Visible && (OverlayActivationMode.Value == OverlayActivation.Disabled || blockShow))
             {
                 State.Value = Visibility.Hidden;
                 return;
@@ -180,8 +184,10 @@ namespace osu.Game.Overlays.Toolbar
             switch (action)
             {
                 case GlobalAction.ToggleToolbar:
+                    userToggled = true;
                     ToggleVisibility();
-                    HiddenByUser = State.Value == Visibility.Hidden;
+                    hiddenByUser = State.Value == Visibility.Hidden;
+                    userToggled = false;
                     return true;
             }
 
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 8f1fd627f5..81b1cb0bf1 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -352,8 +352,7 @@ namespace osu.Game.Screens.Menu
                                 if (impact)
                                     logo.Impact();
 
-                                if (game?.Toolbar.HiddenByUser == false)
-                                    game.Toolbar.Show();
+                                game?.Toolbar.Show();
                             }, 200);
                             break;
 

From b13f193c8dd60573aa579bb47b06d1d02ed0b5ef Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sat, 13 Mar 2021 19:26:38 +0900
Subject: [PATCH 219/434] Fix incorrect task being returned for changelog
 continuations

---
 osu.Game/Overlays/ChangelogOverlay.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 2da5be5e6c..eda7748367 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -163,7 +163,7 @@ namespace osu.Game.Overlays
                 await API.PerformAsync(req).ConfigureAwait(false);
 
                 return tcs.Task;
-            });
+            }).Unwrap();
         }
 
         private CancellationTokenSource loadContentCancellation;

From 4afbccfcff2544fa4a4e80d765a2bccb270c1658 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sat, 13 Mar 2021 19:30:40 +0900
Subject: [PATCH 220/434] Fix initial operation potentially running before DI
 is completed

---
 osu.Game/Overlays/ChangelogOverlay.cs | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index eda7748367..e7d68853ad 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Overlays
 {
     public class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
     {
+        public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
         public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
 
         private Sample sampleBack;
@@ -126,8 +128,11 @@ namespace osu.Game.Overlays
 
         private Task initialFetchTask;
 
-        private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ =>
-            Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
+        private void performAfterFetch(Action action) => Schedule(() =>
+        {
+            fetchListing()?.ContinueWith(_ =>
+                Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
+        });
 
         private Task fetchListing()
         {

From e70ba2d005c45bb485d3c985873fbecf99a1c71c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sat, 13 Mar 2021 23:29:01 +0900
Subject: [PATCH 221/434] Remove unnecessary second variable

---
 osu.Game/Overlays/Toolbar/Toolbar.cs | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 7497f4d210..5e2280e2fc 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -28,8 +28,6 @@ namespace osu.Game.Overlays.Toolbar
         /// </summary>
         private bool hiddenByUser;
 
-        private bool userToggled;
-
         public Action OnHome;
 
         private ToolbarUserButton userButton;
@@ -151,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar
 
         protected override void UpdateState(ValueChangedEvent<Visibility> state)
         {
-            var blockShow = !userToggled && hiddenByUser;
+            bool blockShow = hiddenByUser || OverlayActivationMode.Value == OverlayActivation.Disabled;
 
-            if (state.NewValue == Visibility.Visible && (OverlayActivationMode.Value == OverlayActivation.Disabled || blockShow))
+            if (state.NewValue == Visibility.Visible && blockShow)
             {
                 State.Value = Visibility.Hidden;
                 return;
@@ -184,10 +182,8 @@ namespace osu.Game.Overlays.Toolbar
             switch (action)
             {
                 case GlobalAction.ToggleToolbar:
-                    userToggled = true;
+                    hiddenByUser = State.Value == Visibility.Visible; // set before toggling to allow the operation to always succeed.
                     ToggleVisibility();
-                    hiddenByUser = State.Value == Visibility.Hidden;
-                    userToggled = false;
                     return true;
             }
 

From a227b0a581103004c10e767f0511f01bb8af2fbc Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sat, 13 Mar 2021 23:29:47 +0900
Subject: [PATCH 222/434] Build on xmldoc with rationale

---
 osu.Game/Overlays/Toolbar/Toolbar.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 5e2280e2fc..d049c2d3ec 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -25,6 +25,7 @@ namespace osu.Game.Overlays.Toolbar
 
         /// <summary>
         /// Whether the user hid this <see cref="Toolbar"/> with <see cref="GlobalAction.ToggleToolbar"/>.
+        /// In this state, automatic toggles should not occur, respecting the user's preference to have no toolbar.
         /// </summary>
         private bool hiddenByUser;
 

From 779c55d768a6723c1735ef92a26fedaec1dd6760 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Sat, 13 Mar 2021 06:05:08 +0300
Subject: [PATCH 223/434] Fix potentially adding null legacy text to hierarchy

---
 .../Skinning/Legacy/LegacySpinner.cs          | 41 +++++++++++--------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index b0b9cba2bd..610eb54316 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -47,7 +47,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            AddInternal(new Container
+            Container overlayContainer;
+
+            AddInternal(overlayContainer = new Container
             {
                 Depth = float.MinValue,
                 RelativeSizeAxes = Axes.Both,
@@ -70,17 +72,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         Scale = new Vector2(SPRITE_SCALE),
                         Y = SPINNER_TOP_OFFSET + 115,
                     },
-                    bonusCounter = ((LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s =>
-                    {
-                        s.Alpha = 0f;
-                        s.Anchor = Anchor.TopCentre;
-                        s.Origin = Anchor.Centre;
-                        s.Font = s.Font.With(fixedWidth: false);
-                        s.Scale = new Vector2(SPRITE_SCALE);
-                        s.Y = SPINNER_TOP_OFFSET + 299;
-                    }),
                 }
             });
+
+            bonusCounter = (LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText));
+
+            if (bonusCounter != null)
+            {
+                bonusCounter.Alpha = 0f;
+                bonusCounter.Anchor = Anchor.TopCentre;
+                bonusCounter.Origin = Anchor.Centre;
+                bonusCounter.Font = bonusCounter.Font.With(fixedWidth: false);
+                bonusCounter.Scale = new Vector2(SPRITE_SCALE);
+                bonusCounter.Y = SPINNER_TOP_OFFSET + 299;
+                overlayContainer.Add(bonusCounter);
+            }
         }
 
         private IBindable<double> gainedBonus;
@@ -91,13 +97,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         {
             base.LoadComplete();
 
-            gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
-            gainedBonus.BindValueChanged(bonus =>
+            if (bonusCounter != null)
             {
-                bonusCounter.Text = $"{bonus.NewValue}";
-                bonusCounter.FadeOutFromOne(800, Easing.Out);
-                bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
-            });
+                gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
+                gainedBonus.BindValueChanged(bonus =>
+                {
+                    bonusCounter.Text = $"{bonus.NewValue}";
+                    bonusCounter.FadeOutFromOne(800, Easing.Out);
+                    bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
+                });
+            }
 
             completed.BindValueChanged(onCompletedChanged, true);
 

From 8b74666cc339416b2b3f443c8dfdbb518c2f2148 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sun, 14 Mar 2021 15:51:38 +0100
Subject: [PATCH 224/434] Add support for pooling explosions in taiko

---
 .../Skinning/TestSceneHitExplosion.cs         |  4 +-
 .../Skinning/Legacy/LegacyHitExplosion.cs     | 51 +++++-------
 .../UI/DefaultHitExplosion.cs                 | 41 +++++++---
 osu.Game.Rulesets.Taiko/UI/HitExplosion.cs    | 80 ++++++++++++++-----
 .../UI/HitExplosionPool.cs                    | 24 ++++++
 osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs   | 23 ++++++
 osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs  | 11 ++-
 7 files changed, 170 insertions(+), 64 deletions(-)
 create mode 100644 osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
 create mode 100644 osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs

diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index fecb5d4a74..ba6e04c92e 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
                     // the hit needs to be added to hierarchy in order for nested objects to be created correctly.
                     // setting zero alpha is supposed to prevent the test from looking broken.
                     hit.With(h => h.Alpha = 0),
-                    new HitExplosion(hit, hit.Type)
+                    new HitExplosion(hit.Type)
                     {
                         Anchor = Anchor.Centre,
                         Origin = Anchor.Centre,
-                    }
+                    }.With(explosion => explosion.Apply(hit))
                 }
             };
         }
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index 651cdd6438..9734e12413 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -1,22 +1,24 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Linq;
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.UI;
 
 namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
 {
-    public class LegacyHitExplosion : CompositeDrawable
+    public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
     {
-        private readonly Drawable sprite;
-        private readonly Drawable strongSprite;
+        public override bool RemoveWhenNotAlive => false;
 
-        private DrawableStrongNestedHit nestedStrongHit;
-        private bool switchedToStrongSprite;
+        private readonly Drawable sprite;
+
+        [CanBeNull]
+        private readonly Drawable strongSprite;
 
         /// <summary>
         /// Creates a new legacy hit explosion.
@@ -27,14 +29,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
         /// </remarks>
         /// <param name="sprite">The normal legacy explosion sprite.</param>
         /// <param name="strongSprite">The strong legacy explosion sprite.</param>
-        public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null)
+        public LegacyHitExplosion(Drawable sprite, [CanBeNull] Drawable strongSprite = null)
         {
             this.sprite = sprite;
             this.strongSprite = strongSprite;
         }
 
         [BackgroundDependencyLoader]
-        private void load(DrawableHitObject judgedObject)
+        private void load()
         {
             Anchor = Anchor.Centre;
             Origin = Anchor.Centre;
@@ -56,17 +58,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
                     s.Origin = Anchor.Centre;
                 }));
             }
-
-            if (judgedObject is DrawableHit hit)
-                nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit;
         }
 
-        protected override void LoadComplete()
+        public void Animate(DrawableHitObject drawableHitObject)
         {
-            base.LoadComplete();
-
             const double animation_time = 120;
 
+            (sprite as IFramedAnimation)?.GotoFrame(0);
+            (strongSprite as IFramedAnimation)?.GotoFrame(0);
+
             this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
 
             this.ScaleTo(0.6f)
@@ -77,24 +77,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
             Expire(true);
         }
 
-        protected override void Update()
+        public void AnimateSecondHit()
         {
-            base.Update();
+            if (strongSprite == null)
+                return;
 
-            if (shouldSwitchToStrongSprite() && !switchedToStrongSprite)
-            {
-                sprite.FadeOut(50, Easing.OutQuint);
-                strongSprite.FadeIn(50, Easing.OutQuint);
-                switchedToStrongSprite = true;
-            }
-        }
-
-        private bool shouldSwitchToStrongSprite()
-        {
-            if (nestedStrongHit == null || strongSprite == null)
-                return false;
-
-            return nestedStrongHit.IsHit;
+            sprite.FadeOut(50, Easing.OutQuint);
+            strongSprite.FadeIn(50, Easing.OutQuint);
         }
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
index 3bd20e4bb4..2519573ce9 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
@@ -13,19 +14,25 @@ using osuTK.Graphics;
 
 namespace osu.Game.Rulesets.Taiko.UI
 {
-    internal class DefaultHitExplosion : CircularContainer
+    internal class DefaultHitExplosion : CircularContainer, IHitExplosion
     {
-        private readonly DrawableHitObject judgedObject;
+        public override bool RemoveWhenNotAlive => false;
+
         private readonly HitResult result;
 
-        public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result)
+        [CanBeNull]
+        private Box body;
+
+        [Resolved]
+        private OsuColour colours { get; set; }
+
+        public DefaultHitExplosion(HitResult result)
         {
-            this.judgedObject = judgedObject;
             this.result = result;
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
+        private void load()
         {
             RelativeSizeAxes = Axes.Both;
 
@@ -40,26 +47,38 @@ namespace osu.Game.Rulesets.Taiko.UI
             if (!result.IsHit())
                 return;
 
-            bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;
-
             InternalChildren = new[]
             {
-                new Box
+                body = new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Colour = isRim ? colours.BlueDarker : colours.PinkDarker,
                 }
             };
+
+            updateColour();
         }
 
-        protected override void LoadComplete()
+        private void updateColour([CanBeNull] DrawableHitObject judgedObject = null)
         {
-            base.LoadComplete();
+            if (body == null)
+                return;
+
+            bool isRim = (judgedObject?.HitObject as Hit)?.Type == HitType.Rim;
+            body.Colour = isRim ? colours.BlueDarker : colours.PinkDarker;
+        }
+
+        public void Animate(DrawableHitObject drawableHitObject)
+        {
+            updateColour(drawableHitObject);
 
             this.ScaleTo(3f, 1000, Easing.OutQuint);
             this.FadeOut(500);
 
             Expire(true);
         }
+
+        public void AnimateSecondHit()
+        {
+        }
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index d1fb3348b9..d2ae36a03e 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -2,10 +2,12 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using JetBrains.Annotations;
 using osuTK;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.Taiko.Objects;
@@ -16,31 +18,35 @@ namespace osu.Game.Rulesets.Taiko.UI
     /// <summary>
     /// A circle explodes from the hit target to indicate a hitobject has been hit.
     /// </summary>
-    internal class HitExplosion : CircularContainer
+    internal class HitExplosion : PoolableDrawable
     {
         public override bool RemoveWhenNotAlive => true;
-
-        [Cached(typeof(DrawableHitObject))]
-        public readonly DrawableHitObject JudgedObject;
+        public override bool RemoveCompletedTransforms => false;
 
         private readonly HitResult result;
 
+        [CanBeNull]
+        public DrawableHitObject JudgedObject;
+
         private SkinnableDrawable skinnable;
 
-        public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
-
-        public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
-
-        public HitExplosion(DrawableHitObject judgedObject, HitResult result)
+        /// <summary>
+        /// This constructor only exists to meet the <c>new()</c> type constraint of <see cref="DrawablePool{T}"/>.
+        /// </summary>
+        public HitExplosion()
+            : this(HitResult.Great)
+        {
+        }
+
+        public HitExplosion(HitResult result)
         {
-            JudgedObject = judgedObject;
             this.result = result;
 
             Anchor = Anchor.Centre;
             Origin = Anchor.Centre;
 
-            RelativeSizeAxes = Axes.Both;
             Size = new Vector2(TaikoHitObject.DEFAULT_SIZE);
+            RelativeSizeAxes = Axes.Both;
 
             RelativePositionAxes = Axes.Both;
         }
@@ -48,7 +54,44 @@ namespace osu.Game.Rulesets.Taiko.UI
         [BackgroundDependencyLoader]
         private void load()
         {
-            Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result));
+            InternalChild = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(result));
+            skinnable.OnSkinChanged += runAnimation;
+        }
+
+        public void Apply([CanBeNull] DrawableHitObject drawableHitObject)
+        {
+            JudgedObject = drawableHitObject;
+        }
+
+        protected override void PrepareForUse()
+        {
+            base.PrepareForUse();
+            runAnimation();
+        }
+
+        protected override void FreeAfterUse()
+        {
+            base.FreeAfterUse();
+
+            // clean up transforms on free instead of on prepare as is usually the case
+            // to avoid potentially overriding the effects of VisualiseSecondHit() in the case it is called before PrepareForUse().
+            ApplyTransformsAt(double.MinValue, true);
+            ClearTransforms(true);
+        }
+
+        private void runAnimation()
+        {
+            if (JudgedObject?.Result == null)
+                return;
+
+            double resultTime = JudgedObject.Result.TimeAbsolute;
+
+            LifetimeStart = resultTime;
+
+            using (BeginAbsoluteSequence(resultTime))
+                (skinnable.Drawable as IHitExplosion)?.Animate(JudgedObject);
+
+            LifetimeEnd = skinnable.Drawable.LatestTransformEndTime;
         }
 
         private static TaikoSkinComponents getComponentName(HitResult result)
@@ -68,12 +111,13 @@ namespace osu.Game.Rulesets.Taiko.UI
             throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}");
         }
 
-        /// <summary>
-        /// Transforms this hit explosion to visualise a secondary hit.
-        /// </summary>
-        public void VisualiseSecondHit()
+        public void VisualiseSecondHit(JudgementResult judgementResult)
         {
-            this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
+            using (BeginAbsoluteSequence(judgementResult.TimeAbsolute))
+            {
+                this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
+                (skinnable.Drawable as IHitExplosion)?.AnimateSecondHit();
+            }
         }
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
new file mode 100644
index 0000000000..badf34554c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+    /// <summary>
+    /// Pool for hit explosions of a specific type.
+    /// </summary>
+    internal class HitExplosionPool : DrawablePool<HitExplosion>
+    {
+        private readonly HitResult hitResult;
+
+        public HitExplosionPool(HitResult hitResult)
+            : base(15)
+        {
+            this.hitResult = hitResult;
+        }
+
+        protected override HitExplosion CreateNewDrawable() => new HitExplosion(hitResult);
+    }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..7af941d1ba
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+    /// <summary>
+    /// Interface for hit explosions shown on the playfield's hit target in taiko.
+    /// </summary>
+    public interface IHitExplosion
+    {
+        /// <summary>
+        /// Shows the hit explosion for the supplied <paramref name="drawableHitObject"/>.
+        /// </summary>
+        void Animate(DrawableHitObject drawableHitObject);
+
+        /// <summary>
+        /// Transforms the hit explosion to visualise a secondary hit.
+        /// </summary>
+        void AnimateSecondHit();
+    }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index d2e7b604bb..46dafc3a30 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI
         private SkinnableDrawable mascot;
 
         private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
+        private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
 
         private ProxyContainer topLevelHitContainer;
         private Container rightArea;
@@ -166,10 +167,15 @@ namespace osu.Game.Rulesets.Taiko.UI
             RegisterPool<SwellTick, DrawableSwellTick>(100);
 
             var hitWindows = new TaikoHitWindows();
+
             foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
+            {
                 judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
+                explosionPools.Add(result, new HitExplosionPool(result));
+            }
 
             AddRangeInternal(judgementPools.Values);
+            AddRangeInternal(explosionPools.Values);
         }
 
         protected override void LoadComplete()
@@ -281,7 +287,7 @@ namespace osu.Game.Rulesets.Taiko.UI
             {
                 case TaikoStrongJudgement _:
                     if (result.IsHit)
-                        hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit();
+                        hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result);
                     break;
 
                 case TaikoDrumRollTickJudgement _:
@@ -315,7 +321,8 @@ namespace osu.Game.Rulesets.Taiko.UI
 
         private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type)
         {
-            hitExplosionContainer.Add(new HitExplosion(drawableObject, result));
+            hitExplosionContainer.Add(explosionPools[result]
+                .Get(explosion => explosion.Apply(drawableObject)));
             if (drawableObject.HitObject.Kiai)
                 kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type));
         }

From 00306c007529f176d92666f445a8ef7219787cea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sun, 14 Mar 2021 15:56:34 +0100
Subject: [PATCH 225/434] Adjust test code after explosion pooling changes

---
 .../Skinning/TestSceneHitExplosion.cs         | 19 ++++++++++++++-----
 .../TestSceneHits.cs                          | 10 +++++-----
 2 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index ba6e04c92e..61ea8b664d 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -2,8 +2,12 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.Taiko.Objects;
 using osu.Game.Rulesets.Taiko.UI;
@@ -13,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
     [TestFixture]
     public class TestSceneHitExplosion : TaikoSkinnableTestScene
     {
+        protected override double TimePerAction => 100;
+
         [Test]
         public void TestNormalHit()
         {
@@ -21,11 +27,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
             AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss))));
         }
 
-        [Test]
-        public void TestStrongHit([Values(false, true)] bool hitBoth)
+        [TestCase(HitResult.Great)]
+        [TestCase(HitResult.Ok)]
+        public void TestStrongHit(HitResult type)
         {
-            AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth))));
-            AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth))));
+            AddStep("create hit", () => SetContents(() => getContentFor(createStrongHit(type))));
+            AddStep("visualise second hit",
+                () => this.ChildrenOfType<HitExplosion>()
+                          .ForEach(e => e.VisualiseSecondHit(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()))));
         }
 
         private Drawable getContentFor(DrawableTestHit hit)
@@ -49,6 +58,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
 
         private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type);
 
-        private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth);
+        private DrawableTestHit createStrongHit(HitResult type) => new DrawableTestStrongHit(Time.Current, type);
     }
 }
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
index 7695ca067b..87c936d386 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs
@@ -11,7 +11,6 @@ using osu.Game.Audio;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.Taiko.Judgements;
 using osu.Game.Rulesets.Taiko.Objects;
@@ -108,12 +107,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
         {
             HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great;
 
-            Hit hit = new Hit();
+            Hit hit = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current };
             var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) };
 
             DrawableRuleset.Playfield.Add(h);
 
-            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
+            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
         }
 
         private void addStrongHitJudgement(bool kiai)
@@ -122,6 +121,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
 
             Hit hit = new Hit
             {
+                StartTime = DrawableRuleset.Playfield.Time.Current,
                 IsStrong = true,
                 Samples = createSamples(strong: true)
             };
@@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
 
             DrawableRuleset.Playfield.Add(h);
 
-            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult });
-            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great });
+            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(hit, new TaikoJudgement()) { Type = hitResult });
+            ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(hit.NestedHitObjects.Single(), new TaikoStrongJudgement()) { Type = HitResult.Great });
         }
 
         private void addMissJudgement()

From 0a1e325fc774f371785ba95b618f71bd0637bb2e Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Sun, 14 Mar 2021 19:34:53 +0300
Subject: [PATCH 226/434] Extract requerying of navigational properties from
 DbContext

---
 osu.Game/Beatmaps/BeatmapManager.cs | 16 +----------
 osu.Game/Database/Extensions.cs     | 44 +++++++++++++++++++++++++++++
 osu.Game/Scoring/ScoreManager.cs    | 14 +--------
 osu.Game/Skinning/SkinManager.cs    |  9 +-----
 4 files changed, 47 insertions(+), 36 deletions(-)
 create mode 100644 osu.Game/Database/Extensions.cs

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 3254f53574..f42fba79cb 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -174,21 +174,7 @@ namespace osu.Game.Beatmaps
             if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
                 throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
 
-            var dbContext = ContextFactory.Get();
-
-            // Workaround System.InvalidOperationException
-            // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-            foreach (var beatmap in beatmapSet.Beatmaps)
-            {
-                beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID);
-            }
-
-            // Workaround System.InvalidOperationException
-            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-            foreach (var file in beatmapSet.Files)
-            {
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
+            beatmapSet.Requery(ContextFactory);
 
             // check if a set already exists with the same online id, delete if it does.
             if (beatmapSet.OnlineBeatmapSetID != null)
diff --git a/osu.Game/Database/Extensions.cs b/osu.Game/Database/Extensions.cs
new file mode 100644
index 0000000000..3af26c348e
--- /dev/null
+++ b/osu.Game/Database/Extensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Scoring;
+
+namespace osu.Game.Database
+{
+    public static class Extensions
+    {
+        public static void Requery(this BeatmapSetInfo beatmapSetInfo, IDatabaseContextFactory databaseContextFactory)
+        {
+            var dbContext = databaseContextFactory.Get();
+
+            foreach (var beatmap in beatmapSetInfo.Beatmaps)
+            {
+                // Workaround System.InvalidOperationException
+                // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+                beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID);
+            }
+
+            beatmapSetInfo.Files.Requery(databaseContextFactory);
+        }
+
+        public static void Requery(this ScoreInfo scoreInfo, IDatabaseContextFactory databaseContextFactory)
+        {
+            scoreInfo.Files.Requery(databaseContextFactory);
+            scoreInfo.Beatmap.BeatmapSet.Files.Requery(databaseContextFactory);
+        }
+
+        public static void Requery<T>(this List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+        {
+            var dbContext = databaseContextFactory.Get();
+
+            foreach (var file in files)
+            {
+                // Workaround System.InvalidOperationException
+                // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+        }
+    }
+}
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index a97c516a1b..1e90ee1ac7 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -54,19 +54,7 @@ namespace osu.Game.Scoring
 
         protected override void PreImport(ScoreInfo model)
         {
-            var dbContext = ContextFactory.Get();
-
-            // Workaround System.InvalidOperationException
-            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-            foreach (var file in model.Files)
-            {
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
-
-            foreach (var file in model.Beatmap.BeatmapSet.Files)
-            {
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
+            model.Requery(ContextFactory);
         }
 
         protected override ScoreInfo CreateModel(ArchiveReader archive)
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 2bb27b60d6..c25f00eccb 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -144,14 +144,7 @@ namespace osu.Game.Skinning
 
         protected override void PreImport(SkinInfo model)
         {
-            var dbContext = ContextFactory.Get();
-
-            // Workaround System.InvalidOperationException
-            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-            foreach (var file in model.Files)
-            {
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
+            model.Files.Requery(ContextFactory);
         }
 
         /// <summary>

From 61d5a6cc57941cab31e2abb0acee00c8fad3f80f Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Sun, 14 Mar 2021 19:47:14 +0300
Subject: [PATCH 227/434] Simplify Microsoft.EntityFrameworkCore.Design
 PackageReference

---
 osu.Desktop/osu.Desktop.csproj | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 4af69c573d..d9d23dea6b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -28,10 +28,7 @@
     <PackageReference Include="System.IO.Packaging" Version="5.0.0" />
     <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4" />
     <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
     <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
   </ItemGroup>

From 28ef64b62a5d873110a163c4275fb48c9c47b262 Mon Sep 17 00:00:00 2001
From: Roman Kapustin <TocoToucanMS@gmail.com>
Date: Sun, 14 Mar 2021 21:43:27 +0300
Subject: [PATCH 228/434] Explicitly specify SingleQuery behavior

---
 osu.Game/Database/OsuDbContext.cs | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 689f248de8..e5ae530018 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -3,7 +3,6 @@
 
 using System;
 using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Diagnostics;
 using Microsoft.Extensions.Logging;
 using osu.Framework.Logging;
 using osu.Framework.Statistics;
@@ -111,8 +110,7 @@ namespace osu.Game.Database
         {
             base.OnConfiguring(optionsBuilder);
             optionsBuilder
-                .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
-                .ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning))
+                .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10).UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
                 .UseLoggerFactory(logger.Value);
         }
 

From 900da7b891c3a74faa3ebbe0f030e9cf3350b273 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 12:42:41 +0900
Subject: [PATCH 229/434] Rename and refactor extenion methods to be easier to
 read

---
 .../Database/DatabaseWorkaroundExtensions.cs  | 48 +++++++++++++++++++
 osu.Game/Database/Extensions.cs               | 44 -----------------
 2 files changed, 48 insertions(+), 44 deletions(-)
 create mode 100644 osu.Game/Database/DatabaseWorkaroundExtensions.cs
 delete mode 100644 osu.Game/Database/Extensions.cs

diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
new file mode 100644
index 0000000000..07ce7e8529
--- /dev/null
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Scoring;
+
+namespace osu.Game.Database
+{
+    public static class DatabaseWorkaroundExtensions
+    {
+        public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory)
+        {
+            switch (model)
+            {
+                case ScoreInfo scoreInfo:
+                    scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory);
+                    scoreInfo.Files.RequeryFiles(contextFactory);
+                    break;
+
+                case BeatmapSetInfo beatmapSetInfo:
+                    var context = contextFactory.Get();
+
+                    foreach (var beatmap in beatmapSetInfo.Beatmaps)
+                    {
+                        // Workaround System.InvalidOperationException
+                        // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+                        beatmap.Ruleset = context.RulesetInfo.Find(beatmap.RulesetID);
+                    }
+
+                    requeryFiles(beatmapSetInfo.Files, contextFactory);
+                    break;
+            }
+        }
+
+        public static void RequeryFiles<T>(this List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+        {
+            var dbContext = databaseContextFactory.Get();
+
+            foreach (var file in files)
+            {
+                // Workaround System.InvalidOperationException
+                // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
+            }
+        }
+    }
+}
diff --git a/osu.Game/Database/Extensions.cs b/osu.Game/Database/Extensions.cs
deleted file mode 100644
index 3af26c348e..0000000000
--- a/osu.Game/Database/Extensions.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using osu.Game.Beatmaps;
-using osu.Game.Scoring;
-
-namespace osu.Game.Database
-{
-    public static class Extensions
-    {
-        public static void Requery(this BeatmapSetInfo beatmapSetInfo, IDatabaseContextFactory databaseContextFactory)
-        {
-            var dbContext = databaseContextFactory.Get();
-
-            foreach (var beatmap in beatmapSetInfo.Beatmaps)
-            {
-                // Workaround System.InvalidOperationException
-                // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-                beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID);
-            }
-
-            beatmapSetInfo.Files.Requery(databaseContextFactory);
-        }
-
-        public static void Requery(this ScoreInfo scoreInfo, IDatabaseContextFactory databaseContextFactory)
-        {
-            scoreInfo.Files.Requery(databaseContextFactory);
-            scoreInfo.Beatmap.BeatmapSet.Files.Requery(databaseContextFactory);
-        }
-
-        public static void Requery<T>(this List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
-        {
-            var dbContext = databaseContextFactory.Get();
-
-            foreach (var file in files)
-            {
-                // Workaround System.InvalidOperationException
-                // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
-        }
-    }
-}

From 2bdffd10044984ea0a5639754b8ecff8f4fc979b Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 12:47:58 +0900
Subject: [PATCH 230/434] Move skin requery logic into extension methods

---
 .../Database/DatabaseWorkaroundExtensions.cs  | 19 +++++++++++++++++--
 osu.Game/Skinning/SkinManager.cs              |  2 +-
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
index 07ce7e8529..39bf358071 100644
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -1,21 +1,36 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
 using osu.Game.Beatmaps;
 using osu.Game.Scoring;
+using osu.Game.Skinning;
 
 namespace osu.Game.Database
 {
+    /// <summary>
+    /// Extension methods which contain workarounds to make EFcore 5.x work with our existing (incorrect) thread safety.
+    /// The intention is to avoid blocking package updates while we consider the future of the database backend, with a potential backend switch imminent.
+    /// </summary>
     public static class DatabaseWorkaroundExtensions
     {
+        /// <summary>
+        /// Re-query the provided model to ensure it is in a sane state. This method requires explicit implementation per model type.
+        /// </summary>
+        /// <param name="model"></param>
+        /// <param name="contextFactory"></param>
         public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory)
         {
             switch (model)
             {
+                case SkinInfo skinInfo:
+                    requeryFiles(skinInfo.Files, contextFactory);
+                    break;
+
                 case ScoreInfo scoreInfo:
                     scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory);
-                    scoreInfo.Files.RequeryFiles(contextFactory);
+                    requeryFiles(scoreInfo.Files, contextFactory);
                     break;
 
                 case BeatmapSetInfo beatmapSetInfo:
@@ -33,7 +48,7 @@ namespace osu.Game.Database
             }
         }
 
-        public static void RequeryFiles<T>(this List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+        private static void requeryFiles<T>(List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
         {
             var dbContext = databaseContextFactory.Get();
 
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index c25f00eccb..601b77e782 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -144,7 +144,7 @@ namespace osu.Game.Skinning
 
         protected override void PreImport(SkinInfo model)
         {
-            model.Files.Requery(ContextFactory);
+            model.Requery(ContextFactory);
         }
 
         /// <summary>

From 8a3553388972349a6833afcf915f44a7dae19995 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 12:48:23 +0900
Subject: [PATCH 231/434] Add fall-through case to catch a potential requery
 for unsupported model type

---
 osu.Game/Database/DatabaseWorkaroundExtensions.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
index 39bf358071..1d5c98ed8d 100644
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Database
 
                     requeryFiles(beatmapSetInfo.Files, contextFactory);
                     break;
+
+                default:
+                    throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model));
             }
         }
 

From 1573298e682329d133059a74dd56c8c849b86bba Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 13:12:10 +0900
Subject: [PATCH 232/434] Update remaining package references to point to
 efcore5

---
 .../osu.Game.Rulesets.Catch.Tests.csproj                        | 2 +-
 .../osu.Game.Rulesets.Mania.Tests.csproj                        | 2 +-
 osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj  | 2 +-
 .../osu.Game.Rulesets.Taiko.Tests.csproj                        | 2 +-
 osu.Game.Tests/osu.Game.Tests.csproj                            | 2 +-
 osu.iOS.props                                                   | 2 --
 6 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 728af5124e..42f70151ac 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index af16f39563..e51b20c9fe 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 3d2d1f3fec..f1f75148ef 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index fa00922706..c9a320bdd5 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index e36b3cdc74..6f8e0fac6f 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -6,7 +6,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
     <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.iOS.props b/osu.iOS.props
index ccd33bf88c..71fcdd45f3 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -90,8 +90,6 @@
   <ItemGroup Label="Transitive Dependencies">
     <PackageReference Include="DiffPlex" Version="1.6.3" />
     <PackageReference Include="Humanizer" Version="2.8.26" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />

From 79d3379f55b2b23dbfdd613c58adc8a8b3259768 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 13:20:22 +0900
Subject: [PATCH 233/434] Reformat application of configuration

---
 osu.Game/Database/OsuDbContext.cs | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index e5ae530018..2342ab07d4 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -110,7 +110,10 @@ namespace osu.Game.Database
         {
             base.OnConfiguring(optionsBuilder);
             optionsBuilder
-                .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10).UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
+                .UseSqlite(connectionString,
+                    sqliteOptions => sqliteOptions
+                                     .CommandTimeout(10)
+                                     .UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
                 .UseLoggerFactory(logger.Value);
         }
 

From 2904f479c65bf9f2cd76a3164ab51d13ff53dc96 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 13:26:14 +0900
Subject: [PATCH 234/434] Share file lookup workaround in ArchiveModelManager
 with workaround extensions class

---
 osu.Game/Database/ArchiveModelManager.cs      |  4 +---
 .../Database/DatabaseWorkaroundExtensions.cs  | 23 +++++++++++--------
 2 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index fe2caaa0b7..31c365b478 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -462,9 +462,7 @@ namespace osu.Game.Database
                 // Dereference the existing file info, since the file model will be removed.
                 if (file.FileInfo != null)
                 {
-                    // Workaround System.InvalidOperationException
-                    // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-                    file.FileInfo = usage.Context.FileInfo.Find(file.FileInfoID);
+                    file.Requery(usage.Context);
 
                     Files.Dereference(file.FileInfo);
 
diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
index 1d5c98ed8d..8ac05f78e0 100644
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -49,18 +49,23 @@ namespace osu.Game.Database
                 default:
                     throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model));
             }
+
+            void requeryFiles<T>(List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+            {
+                var dbContext = databaseContextFactory.Get();
+
+                foreach (var file in files)
+                {
+                    Requery(file, dbContext);
+                }
+            }
         }
 
-        private static void requeryFiles<T>(List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
+        public static void Requery(this INamedFileInfo file, OsuDbContext dbContext)
         {
-            var dbContext = databaseContextFactory.Get();
-
-            foreach (var file in files)
-            {
-                // Workaround System.InvalidOperationException
-                // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-                file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-            }
+            // Workaround System.InvalidOperationException
+            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
+            file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
         }
     }
 }

From fce21f23d687ab72be391c23987ad1edff8740f0 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 13:29:26 +0900
Subject: [PATCH 235/434] Add comments marking workarounds required for EFcore
 5

---
 .../Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs   | 2 +-
 .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs        | 2 +-
 osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs     | 2 +-
 osu.Game/Database/ArchiveModelManager.cs                        | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 4a9eaa1842..8cfe5d8af2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
                 beatmaps.Add(new BeatmapInfo
                 {
                     Ruleset = rulesets.GetRuleset(i % 4),
-                    RulesetID = i % 4,
+                    RulesetID = i % 4, // workaround for efcore 5 compatibility.
                     OnlineBeatmapID = beatmapId,
                     Length = length,
                     BPM = bpm,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 223ace6ca5..9b8b74e6f6 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Metadata = metadata,
                     BaseDifficulty = new BeatmapDifficulty(),
                     Ruleset = ruleset,
-                    RulesetID = ruleset.ID.GetValueOrDefault(),
+                    RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
                     StarDifficulty = difficultyIndex + 1,
                     Version = $"SR{difficultyIndex + 1}"
                 }).ToList()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 4b402d0c54..2d192ae207 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -915,7 +915,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 beatmaps.Add(new BeatmapInfo
                 {
                     Ruleset = ruleset,
-                    RulesetID = ruleset.ID.GetValueOrDefault(),
+                    RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
                     OnlineBeatmapID = beatmapId,
                     Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
                     Length = length,
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 31c365b478..64428882ac 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -642,7 +642,7 @@ namespace osu.Game.Database
                     {
                         Filename = file.Substring(prefix.Length).ToStandardisedPath(),
                         FileInfo = fileInfo,
-                        FileInfoID = fileInfo.ID
+                        FileInfoID = fileInfo.ID // workaround for efcore 5 compatibility.
                     });
                 }
             }

From 6d4c1ba2aee43c9fdab49f83ba16c0bcdedcce78 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 13:35:08 +0900
Subject: [PATCH 236/434] Fix a couple of new inspections introduced in Rider
 EAPs

---
 osu.Game/Beatmaps/BeatmapConverter.cs             | 10 +++++-----
 osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs |  3 ---
 2 files changed, 5 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index cb0b3a8d09..b291edd19d 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -17,12 +17,12 @@ namespace osu.Game.Beatmaps
     public abstract class BeatmapConverter<T> : IBeatmapConverter
         where T : HitObject
     {
-        private event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
+        private event Action<HitObject, IEnumerable<HitObject>> objectConverted;
 
         event Action<HitObject, IEnumerable<HitObject>> IBeatmapConverter.ObjectConverted
         {
-            add => ObjectConverted += value;
-            remove => ObjectConverted -= value;
+            add => objectConverted += value;
+            remove => objectConverted -= value;
         }
 
         public IBeatmap Beatmap { get; }
@@ -92,10 +92,10 @@ namespace osu.Game.Beatmaps
 
                 var converted = ConvertHitObject(obj, beatmap, cancellationToken);
 
-                if (ObjectConverted != null)
+                if (objectConverted != null)
                 {
                     converted = converted.ToList();
-                    ObjectConverted.Invoke(obj, converted);
+                    objectConverted.Invoke(obj, converted);
                 }
 
                 foreach (var c in converted)
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index df940e8c8e..d06478b9de 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -471,9 +471,6 @@ namespace osu.Game.Beatmaps.Formats
 
         private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
         {
-            if (hitSampleInfo == null)
-                return "0";
-
             if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
                 return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture);
 

From 1e519f0d31125a3bb508be2dd97777556f69f0b1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 14:20:59 +0900
Subject: [PATCH 237/434] Fix seemingly innocent logic change causing breakage
 in score imports

---
 osu.Game/Database/DatabaseWorkaroundExtensions.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
index 8ac05f78e0..a3a982f232 100644
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Database
                     break;
 
                 case ScoreInfo scoreInfo:
-                    scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory);
+                    requeryFiles(scoreInfo.Beatmap.BeatmapSet.Files, contextFactory);
                     requeryFiles(scoreInfo.Files, contextFactory);
                     break;
 

From 3dd72d6f7d229bd7cb8311e707ace797fed31d3e Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Sun, 14 Mar 2021 22:47:05 -0700
Subject: [PATCH 238/434] Fix disable mouse buttons setting not showing default
 indicator when using keybind

---
 osu.Game/OsuGame.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index dd775888a1..eb34ba4a37 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -886,7 +886,9 @@ namespace osu.Game
                     return true;
 
                 case GlobalAction.ToggleGameplayMouseButtons:
-                    LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get<bool>(OsuSetting.MouseDisableButtons));
+                    var mouseDisableButtons = LocalConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons);
+
+                    mouseDisableButtons.Value = !mouseDisableButtons.Value;
                     return true;
 
                 case GlobalAction.RandomSkin:

From 848adddd9285c3b22c93e6fa06f12c2e9f825c48 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Mon, 15 Mar 2021 11:05:29 +0300
Subject: [PATCH 239/434] Use `double.ToString(InvariantInfo)` instead

---
 osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs | 3 ++-
 osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs   | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
index 83676d3784..891821fe2f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinner.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Globalization;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
             gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
             gainedBonus.BindValueChanged(bonus =>
             {
-                bonusCounter.Text = $"{bonus.NewValue}";
+                bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
                 bonusCounter.FadeOutFromOne(1500);
                 bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
             });
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 610eb54316..6d4fbd7445 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Globalization;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                 gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
                 gainedBonus.BindValueChanged(bonus =>
                 {
-                    bonusCounter.Text = $"{bonus.NewValue}";
+                    bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
                     bonusCounter.FadeOutFromOne(800, Easing.Out);
                     bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
                 });

From 393f1fbd3f7f4169fefb1df16b689e66314e6fdf Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 15 Mar 2021 10:07:50 -0700
Subject: [PATCH 240/434] Remove skype

---
 osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 1 -
 osu.Game/Users/User.cs                                    | 3 ---
 2 files changed, 4 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 2925107766..662f55317b 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -138,7 +138,6 @@ namespace osu.Game.Overlays.Profile.Header
             if (!string.IsNullOrEmpty(user.Twitter))
                 anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}");
             anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord);
-            anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat");
             anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website);
 
             // If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 4d537b91bd..6c45417db0 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -111,9 +111,6 @@ namespace osu.Game.Users
         [JsonProperty(@"twitter")]
         public string Twitter;
 
-        [JsonProperty(@"skype")]
-        public string Skype;
-
         [JsonProperty(@"discord")]
         public string Discord;
 

From 58220481dbd71edf491a56494bafa302f8250915 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 15 Mar 2021 20:38:11 +0100
Subject: [PATCH 241/434] Rename `I{-> Animatable}HitExplosion`

---
 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs | 2 +-
 osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs             | 2 +-
 osu.Game.Rulesets.Taiko/UI/HitExplosion.cs                    | 4 ++--
 .../UI/{IHitExplosion.cs => IAnimatableHitExplosion.cs}       | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)
 rename osu.Game.Rulesets.Taiko/UI/{IHitExplosion.cs => IAnimatableHitExplosion.cs} (79%)

diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index 9734e12413..aad9f53b93 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Taiko.UI;
 
 namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
 {
-    public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
+    public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion
     {
         public override bool RemoveWhenNotAlive => false;
 
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
index 2519573ce9..5bb463353d 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -14,7 +14,7 @@ using osuTK.Graphics;
 
 namespace osu.Game.Rulesets.Taiko.UI
 {
-    internal class DefaultHitExplosion : CircularContainer, IHitExplosion
+    internal class DefaultHitExplosion : CircularContainer, IAnimatableHitExplosion
     {
         public override bool RemoveWhenNotAlive => false;
 
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index d2ae36a03e..bdebe9da17 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.UI
             LifetimeStart = resultTime;
 
             using (BeginAbsoluteSequence(resultTime))
-                (skinnable.Drawable as IHitExplosion)?.Animate(JudgedObject);
+                (skinnable.Drawable as IAnimatableHitExplosion)?.Animate(JudgedObject);
 
             LifetimeEnd = skinnable.Drawable.LatestTransformEndTime;
         }
@@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.UI
             using (BeginAbsoluteSequence(judgementResult.TimeAbsolute))
             {
                 this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
-                (skinnable.Drawable as IHitExplosion)?.AnimateSecondHit();
+                (skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit();
             }
         }
     }
diff --git a/osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
similarity index 79%
rename from osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs
rename to osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
index 7af941d1ba..cf0f5f9fb6 100644
--- a/osu.Game.Rulesets.Taiko/UI/IHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
@@ -6,9 +6,9 @@ using osu.Game.Rulesets.Objects.Drawables;
 namespace osu.Game.Rulesets.Taiko.UI
 {
     /// <summary>
-    /// Interface for hit explosions shown on the playfield's hit target in taiko.
+    /// A skinnable element of a hit explosion that supports playing an animation from the current point in time.
     /// </summary>
-    public interface IHitExplosion
+    public interface IAnimatableHitExplosion
     {
         /// <summary>
         /// Shows the hit explosion for the supplied <paramref name="drawableHitObject"/>.

From f4e508b57051e887a00a8f4649e4616b762a8c8c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 15 Mar 2021 20:43:30 +0100
Subject: [PATCH 242/434] Remove unnecessary overrides

---
 osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs             | 2 --
 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs | 2 --
 2 files changed, 4 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index 69b81d6d5c..1b5d576c1e 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Mania.UI
     {
         private const float default_large_faint_size = 0.8f;
 
-        public override bool RemoveWhenNotAlive => true;
-
         [Resolved]
         private Column column { get; set; }
 
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index aad9f53b93..bef9279bac 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
 {
     public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion
     {
-        public override bool RemoveWhenNotAlive => false;
-
         private readonly Drawable sprite;
 
         [CanBeNull]

From 72c18fbdfe8f91798a1adf16ca1f92ecbc593ae4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 15 Mar 2021 20:48:19 +0100
Subject: [PATCH 243/434] Restructure explosion animation to avoid resetting
 transforms on free

---
 osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 32 ++++++++++++----------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index bdebe9da17..8f5e9e54ab 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Taiko.UI
 
         private readonly HitResult result;
 
+        private double? secondHitTime;
+
         [CanBeNull]
         public DrawableHitObject JudgedObject;
 
@@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI
         public void Apply([CanBeNull] DrawableHitObject drawableHitObject)
         {
             JudgedObject = drawableHitObject;
+            secondHitTime = null;
         }
 
         protected override void PrepareForUse()
@@ -69,16 +72,6 @@ namespace osu.Game.Rulesets.Taiko.UI
             runAnimation();
         }
 
-        protected override void FreeAfterUse()
-        {
-            base.FreeAfterUse();
-
-            // clean up transforms on free instead of on prepare as is usually the case
-            // to avoid potentially overriding the effects of VisualiseSecondHit() in the case it is called before PrepareForUse().
-            ApplyTransformsAt(double.MinValue, true);
-            ClearTransforms(true);
-        }
-
         private void runAnimation()
         {
             if (JudgedObject?.Result == null)
@@ -88,9 +81,21 @@ namespace osu.Game.Rulesets.Taiko.UI
 
             LifetimeStart = resultTime;
 
+            ApplyTransformsAt(double.MinValue, true);
+            ClearTransforms(true);
+
             using (BeginAbsoluteSequence(resultTime))
                 (skinnable.Drawable as IAnimatableHitExplosion)?.Animate(JudgedObject);
 
+            if (secondHitTime != null)
+            {
+                using (BeginAbsoluteSequence(secondHitTime.Value))
+                {
+                    this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
+                    (skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit();
+                }
+            }
+
             LifetimeEnd = skinnable.Drawable.LatestTransformEndTime;
         }
 
@@ -113,11 +118,8 @@ namespace osu.Game.Rulesets.Taiko.UI
 
         public void VisualiseSecondHit(JudgementResult judgementResult)
         {
-            using (BeginAbsoluteSequence(judgementResult.TimeAbsolute))
-            {
-                this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50);
-                (skinnable.Drawable as IAnimatableHitExplosion)?.AnimateSecondHit();
-            }
+            secondHitTime = judgementResult.TimeAbsolute;
+            runAnimation();
         }
     }
 }

From da3dc61aae29202b7a0ebb498c274852c1d955ab Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 16 Mar 2021 10:58:42 +0900
Subject: [PATCH 244/434] Remove newline

---
 osu.Game/OsuGame.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index eb34ba4a37..7d11029a9c 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -887,7 +887,6 @@ namespace osu.Game
 
                 case GlobalAction.ToggleGameplayMouseButtons:
                     var mouseDisableButtons = LocalConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons);
-
                     mouseDisableButtons.Value = !mouseDisableButtons.Value;
                     return true;
 

From c7740d1181a165466b9960ed54f10c7e14e85075 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 15:52:24 +0900
Subject: [PATCH 245/434] Fix opening the editor occasionally causing a hard
 crash due to incorrect threading logic

Setting one of the global screen `Bindable`s (in this case, `Beatmap`)
is not valid from anywhere but the update thread. This changes the order
in which things happen during the editor startup process to ensure
correctness.

Closes #11968.
---
 osu.Game/Screens/Edit/Editor.cs | 67 +++++++++++++++++++++------------
 1 file changed, 42 insertions(+), 25 deletions(-)

diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 0ba202b082..3a4c3491ff 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -106,26 +106,29 @@ namespace osu.Game.Screens.Edit
         [BackgroundDependencyLoader]
         private void load(OsuColour colours, GameHost host, OsuConfigManager config)
         {
-            if (Beatmap.Value is DummyWorkingBeatmap)
+            var loadableBeatmap = Beatmap.Value;
+
+            if (loadableBeatmap is DummyWorkingBeatmap)
             {
                 isNewBeatmap = true;
 
-                var newBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
+                loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
+
+                // required so we can get the track length in EditorClock.
+                // this is safe as nothing has yet got a reference to this new beatmap.
+                loadableBeatmap.LoadTrack();
 
                 // this is a bit haphazard, but guards against setting the lease Beatmap bindable if
                 // the editor has already been exited.
                 if (!ValidForPush)
                     return;
-
-                // this probably shouldn't be set in the asynchronous load method, but everything following relies on it.
-                Beatmap.Value = newBeatmap;
             }
 
-            beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor;
-            beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
+            beatDivisor.Value = loadableBeatmap.BeatmapInfo.BeatDivisor;
+            beatDivisor.BindValueChanged(divisor => loadableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
 
             // Todo: should probably be done at a DrawableRuleset level to share logic with Player.
-            clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
+            clock = new EditorClock(loadableBeatmap, beatDivisor) { IsCoupled = false };
 
             UpdateClockSource();
 
@@ -139,7 +142,7 @@ namespace osu.Game.Screens.Edit
 
             try
             {
-                playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
+                playableBeatmap = loadableBeatmap.GetPlayableBeatmap(loadableBeatmap.BeatmapInfo.Ruleset);
 
                 // clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages.
                 // eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases.
@@ -153,13 +156,21 @@ namespace osu.Game.Screens.Edit
                 return;
             }
 
-            AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin));
+            AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.Skin));
             dependencies.CacheAs(editorBeatmap);
             changeHandler = new EditorChangeHandler(editorBeatmap);
             dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
 
             updateLastSavedHash();
 
+            Schedule(() =>
+            {
+                // we need to avoid changing the beatmap from an asynchronous load thread. it can potentially cause weirdness including crashes.
+                // 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;
+            });
+
             OsuMenuItem undoMenuItem;
             OsuMenuItem redoMenuItem;
 
@@ -167,17 +178,6 @@ namespace osu.Game.Screens.Edit
             EditorMenuItem copyMenuItem;
             EditorMenuItem pasteMenuItem;
 
-            var fileMenuItems = new List<MenuItem>
-            {
-                new EditorMenuItem("Save", MenuItemType.Standard, Save)
-            };
-
-            if (RuntimeInfo.IsDesktop)
-                fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
-
-            fileMenuItems.Add(new EditorMenuItemSpacer());
-            fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
-
             AddInternal(new OsuContextMenuContainer
             {
                 RelativeSizeAxes = Axes.Both,
@@ -209,7 +209,7 @@ namespace osu.Game.Screens.Edit
                             {
                                 new MenuItem("File")
                                 {
-                                    Items = fileMenuItems
+                                    Items = createFileMenuItems()
                                 },
                                 new MenuItem("Edit")
                                 {
@@ -242,7 +242,11 @@ namespace osu.Game.Screens.Edit
                         Height = 60,
                         Children = new Drawable[]
                         {
-                            bottomBackground = new Box { RelativeSizeAxes = Axes.Both },
+                            bottomBackground = new Box
+                            {
+                                RelativeSizeAxes = Axes.Both,
+                                Colour = colours.Gray2
+                            },
                             new Container
                             {
                                 RelativeSizeAxes = Axes.Both,
@@ -299,8 +303,6 @@ namespace osu.Game.Screens.Edit
             clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue));
 
             menuBar.Mode.ValueChanged += onModeChanged;
-
-            bottomBackground.Colour = colours.Gray2;
         }
 
         /// <summary>
@@ -681,6 +683,21 @@ namespace osu.Game.Screens.Edit
             lastSavedHash = changeHandler.CurrentStateHash;
         }
 
+        private List<MenuItem> createFileMenuItems()
+        {
+            var fileMenuItems = new List<MenuItem>
+            {
+                new EditorMenuItem("Save", MenuItemType.Standard, Save)
+            };
+
+            if (RuntimeInfo.IsDesktop)
+                fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap));
+
+            fileMenuItems.Add(new EditorMenuItemSpacer());
+            fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
+            return fileMenuItems;
+        }
+
         public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
 
         public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);

From 7fa5fd56475124fa78c84cbb18e3c19838d98e5a Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 17 Mar 2021 16:10:16 +0900
Subject: [PATCH 246/434] Update usages of config with framework changes

---
 .../TestSceneCatchModHidden.cs                |   2 +-
 .../TestSceneCatcher.cs                       |   4 +-
 .../ManiaRulesetConfigManager.cs              |   4 +-
 .../TestSceneDrawableJudgement.cs             |   4 +-
 .../TestSceneGameplayCursor.cs                |  12 +-
 .../TestSceneSkinFallbacks.cs                 |  20 +--
 .../Configuration/OsuRulesetConfigManager.cs  |   8 +-
 .../Input/ConfineMouseTrackerTest.cs          |   2 +-
 .../NonVisual/CustomDataDirectoryTest.cs      |   4 +-
 .../TestSceneSeasonalBackgroundLoader.cs      |   4 +-
 .../Visual/Gameplay/TestSceneFailingLayer.cs  |  12 +-
 .../Visual/Gameplay/TestSceneHUDOverlay.cs    |   8 +-
 .../TestSceneStoryboardSamplePlayback.cs      |   2 +-
 .../Visual/Navigation/OsuGameTestScene.cs     |   4 +-
 .../Navigation/TestSettingsMigration.cs       |   6 +-
 .../SongSelect/TestScenePlaySongSelect.cs     |  24 +--
 .../TestSceneBeatmapListingSearchControl.cs   |   4 +-
 .../UserInterface/TestSceneOnScreenDisplay.cs |  12 +-
 .../NonVisual/CustomTourneyDirectoryTest.cs   |   2 +-
 osu.Game.Tournament/IO/TournamentStorage.cs   |   2 +-
 .../Components/DrawingsConfigManager.cs       |   4 +-
 osu.Game/Configuration/OsuConfigManager.cs    | 142 +++++++++---------
 osu.Game/Configuration/SessionStatics.cs      |   8 +-
 .../Configuration/StorageConfigManager.cs     |   2 +-
 osu.Game/IO/OsuStorage.cs                     |   4 +-
 osu.Game/Online/API/APIAccess.cs              |   4 +-
 osu.Game/Updater/UpdateManager.cs             |   2 +-
 27 files changed, 153 insertions(+), 153 deletions(-)

diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
index f15da29993..1248409b2a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         [BackgroundDependencyLoader]
         private void load()
         {
-            LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+            LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false);
         }
 
         [Test]
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index e8bb57cdf3..48efd73222 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         public void TestHitLightingColour()
         {
             var fruitColour = SkinConfiguration.DefaultComboColours[1];
-            AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true));
+            AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
             AddStep("catch fruit", () => attemptCatch(new Fruit()));
             AddAssert("correct hit lighting colour", () =>
                 catcher.ChildrenOfType<HitExplosion>().First()?.ObjectColour == fruitColour);
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         [Test]
         public void TestHitLightingDisabled()
         {
-            AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false));
+            AddStep("disable hit lighting", () => config.SetValue(OsuSetting.HitLighting, false));
             AddStep("catch fruit", () => attemptCatch(new Fruit()));
             AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
         }
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index 756f2b7b2f..39d0f4bae4 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Configuration
         {
             base.InitialiseDefaults();
 
-            Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
-            Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
+            SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
+            SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
         }
 
         public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index e4158d8f07..4395ca6281 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Tests
         [Test]
         public void TestHitLightingDisabled()
         {
-            AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
+            AddStep("hit lighting disabled", () => config.SetValue(OsuSetting.HitLighting, false));
 
             showResult(HitResult.Great);
 
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests
         [Test]
         public void TestHitLightingEnabled()
         {
-            AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
+            AddStep("hit lighting enabled", () => config.SetValue(OsuSetting.HitLighting, true));
 
             showResult(HitResult.Great);
 
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 461779b185..e3ccf83715 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
 
             AddSliderStep("circle size", 0f, 10f, 0f, val =>
             {
-                config.Set(OsuSetting.AutoCursorSize, true);
+                config.SetValue(OsuSetting.AutoCursorSize, true);
                 gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
                 Scheduler.AddOnce(recreate);
             });
@@ -64,21 +64,21 @@ namespace osu.Game.Rulesets.Osu.Tests
         [TestCase(10, 1.5f)]
         public void TestSizing(int circleSize, float userScale)
         {
-            AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+            AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
             AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
-            AddStep("turn on autosizing", () => config.Set(OsuSetting.AutoCursorSize, true));
+            AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
 
             AddStep("load content", loadContent);
 
             AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
 
-            AddStep("set user scale to 1", () => config.Set(OsuSetting.GameplayCursorSize, 1f));
+            AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f));
             AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
 
-            AddStep("turn off autosizing", () => config.Set(OsuSetting.AutoCursorSize, false));
+            AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false));
             AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
 
-            AddStep($"set user scale to {userScale}", () => config.Set(OsuSetting.GameplayCursorSize, userScale));
+            AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
             AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
         }
 
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 8dbb48c048..56f6fb85fa 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Osu.Tests
         {
             AddStep("enable user provider", () => testUserSkin.Enabled = true);
 
-            AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true));
+            AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
             checkNextHitObject("beatmap");
 
-            AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false));
+            AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
             checkNextHitObject("user");
 
             AddStep("disable user provider", () => testUserSkin.Enabled = false);
@@ -57,20 +57,20 @@ namespace osu.Game.Rulesets.Osu.Tests
         {
             AddStep("enable user provider", () => testUserSkin.Enabled = true);
 
-            AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true));
-            AddStep("enable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, true));
+            AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
+            AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true));
             checkNextHitObject("beatmap");
 
-            AddStep("enable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, true));
-            AddStep("disable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, false));
+            AddStep("enable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, true));
+            AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false));
             checkNextHitObject("beatmap");
 
-            AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false));
-            AddStep("enable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, true));
+            AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
+            AddStep("enable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, true));
             checkNextHitObject("user");
 
-            AddStep("disable beatmap skin", () => LocalConfig.Set<bool>(OsuSetting.BeatmapSkins, false));
-            AddStep("disable beatmap colours", () => LocalConfig.Set<bool>(OsuSetting.BeatmapColours, false));
+            AddStep("disable beatmap skin", () => LocalConfig.SetValue(OsuSetting.BeatmapSkins, false));
+            AddStep("disable beatmap colours", () => LocalConfig.SetValue(OsuSetting.BeatmapColours, false));
             checkNextHitObject("user");
 
             AddStep("disable user provider", () => testUserSkin.Enabled = false);
diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index e8272057f3..9589fd576f 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Configuration
         protected override void InitialiseDefaults()
         {
             base.InitialiseDefaults();
-            Set(OsuRulesetSetting.SnakingInSliders, true);
-            Set(OsuRulesetSetting.SnakingOutSliders, true);
-            Set(OsuRulesetSetting.ShowCursorTrail, true);
-            Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
+            SetDefault(OsuRulesetSetting.SnakingInSliders, true);
+            SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
+            SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
+            SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
         }
     }
 
diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
index b90382488f..27cece42e8 100644
--- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
+++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs
@@ -88,7 +88,7 @@ namespace osu.Game.Tests.Input
             => AddStep($"make window {mode}", () => frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = mode);
 
         private void setGameSideModeTo(OsuConfineMouseMode mode)
-            => AddStep($"set {mode} game-side", () => Game.LocalConfig.Set(OsuSetting.ConfineMouseMode, mode));
+            => AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode));
 
         private void setLocalUserPlayingTo(bool playing)
             => AddStep($"local user {(playing ? "playing" : "not playing")}", () => Game.LocalUserPlaying.Value = playing);
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 045246e5ed..a763544c37 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.NonVisual
             using (var host = new CustomTestHeadlessGameHost())
             {
                 using (var storageConfig = new StorageConfigManager(host.InitialStorage))
-                    storageConfig.Set(StorageConfig.FullPath, customPath);
+                    storageConfig.SetValue(StorageConfig.FullPath, customPath);
 
                 try
                 {
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.NonVisual
             using (var host = new CustomTestHeadlessGameHost())
             {
                 using (var storageConfig = new StorageConfigManager(host.InitialStorage))
-                    storageConfig.Set(StorageConfig.FullPath, customPath);
+                    storageConfig.SetValue(StorageConfig.FullPath, customPath);
 
                 try
                 {
diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
index fba0d92d4b..e7cf830db0 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Background
         public void SetUp() => Schedule(() =>
         {
             // reset API response in statics to avoid test crosstalk.
-            statics.Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
+            statics.SetValue<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
             textureStore.PerformedLookups.Clear();
             dummyAPI.SetState(APIState.Online);
 
@@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Background
             });
 
         private void setSeasonalBackgroundMode(SeasonalBackgroundMode mode)
-            => AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode));
+            => AddStep($"set seasonal mode to {mode}", () => config.SetValue(OsuSetting.SeasonalBackgroundMode, mode));
 
         private void createLoader()
             => AddStep("create loader", () =>
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
index 1c55595c97..5a1a9d3d87 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             });
 
             AddStep("show health", () => showHealth.Value = true);
-            AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+            AddStep("enable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
             AddUntilStep("layer is visible", () => layer.IsPresent);
         }
 
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestLayerDisabledViaConfig()
         {
-            AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+            AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
             AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
             AddUntilStep("layer is not visible", () => !layer.IsPresent);
         }
@@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
 
             AddStep("don't show health", () => showHealth.Value = false);
-            AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+            AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
             AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
 
             AddStep("don't show health", () => showHealth.Value = false);
-            AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+            AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
             AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
 
             AddStep("show health", () => showHealth.Value = true);
-            AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+            AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
             AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
 
             AddStep("show health", () => showHealth.Value = true);
-            AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+            AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
             AddUntilStep("layer fade is visible", () => layer.IsPresent);
         }
     }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index f9914e0193..3cefb8623f 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
 
-            AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+            AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
 
             AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
 
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft));
             AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
 
-            AddStep("set original config value", () => config.Set(OsuSetting.HUDVisibilityMode, originalConfigValue));
+            AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
         }
 
         [Test]
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
 
             AddStep("set keycounter visible false", () =>
             {
-                config.Set<bool>(OsuSetting.KeyOverlay, false);
+                config.SetValue(OsuSetting.KeyOverlay, false);
                 hudOverlay.KeyCounter.AlwaysVisible.Value = false;
             });
 
@@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
             AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
 
-            AddStep("return value", () => config.Set<bool>(OsuSetting.KeyOverlay, keyCounterVisibleValue));
+            AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue));
         }
 
         private void createNew(Action<HUDOverlay> action = null)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
index 1544f8fd35..a718a98aa6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [BackgroundDependencyLoader]
         private void load(OsuConfigManager config)
         {
-            config.Set(OsuSetting.ShowStoryboard, true);
+            config.SetValue(OsuSetting.ShowStoryboard, true);
 
             storyboard = new Storyboard();
             var backgroundLayer = storyboard.GetLayer("Background");
diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
index 96393cc4c3..6ca7707906 100644
--- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
+++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Navigation
 
             // todo: this can be removed once we can run audio tracks without a device present
             // see https://github.com/ppy/osu/issues/1302
-            Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles);
+            Game.LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles);
 
             Add(Game);
         }
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Navigation
                 base.LoadComplete();
                 API.Login("Rhythm Champion", "osu!");
 
-                Dependencies.Get<SessionStatics>().Set(Static.MutedAudioNotificationShownOnce, true);
+                Dependencies.Get<SessionStatics>().SetValue(Static.MutedAudioNotificationShownOnce, true);
             }
         }
 
diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
index c0b77b580e..768f2057a2 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
@@ -15,8 +15,8 @@ namespace osu.Game.Tests.Visual.Navigation
 
             using (var config = new OsuConfigManager(LocalStorage))
             {
-                config.Set(OsuSetting.Version, "2020.101.0");
-                config.Set(OsuSetting.DisplayStarsMaximum, 10.0);
+                config.SetValue(OsuSetting.Version, "2020.101.0");
+                config.SetValue(OsuSetting.DisplayStarsMaximum, 10.0);
             }
         }
 
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation
         {
             AddAssert("config has migrated value", () => Precision.AlmostEquals(Game.LocalConfig.Get<double>(OsuSetting.DisplayStarsMaximum), 10.1));
 
-            AddStep("set value again", () => Game.LocalConfig.Set<double>(OsuSetting.DisplayStarsMaximum, 10));
+            AddStep("set value again", () => Game.LocalConfig.SetValue(OsuSetting.DisplayStarsMaximum, 10));
 
             AddStep("force save config", () => Game.LocalConfig.Save());
 
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 2d192ae207..057b539e44 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -207,14 +207,14 @@ namespace osu.Game.Tests.Visual.SongSelect
             addRulesetImportStep(0);
             addRulesetImportStep(0);
 
-            AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+            AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
 
             createSongSelect();
 
             AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
             AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
 
-            AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+            AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
 
             AddStep("return", () => songSelect.MakeCurrent());
             AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
@@ -297,13 +297,13 @@ namespace osu.Game.Tests.Visual.SongSelect
 
             AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
 
-            AddStep(@"Sort by Artist", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Artist));
-            AddStep(@"Sort by Title", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Title));
-            AddStep(@"Sort by Author", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Author));
-            AddStep(@"Sort by DateAdded", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
-            AddStep(@"Sort by BPM", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.BPM));
-            AddStep(@"Sort by Length", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Length));
-            AddStep(@"Sort by Difficulty", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
+            AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
+            AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
+            AddStep(@"Sort by Author", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
+            AddStep(@"Sort by DateAdded", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
+            AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
+            AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
+            AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
         }
 
         [Test]
@@ -470,7 +470,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             changeRuleset(0);
 
             // used for filter check below
-            AddStep("allow convert display", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+            AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
 
             AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null);
 
@@ -648,7 +648,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         {
             int changeCount = 0;
 
-            AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+            AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
             AddStep("bind beatmap changed", () =>
             {
                 Beatmap.ValueChanged += onChange;
@@ -686,7 +686,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             AddAssert("selection changed only fired twice", () => changeCount == 2);
 
             AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange);
-            AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+            AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
 
             // ReSharper disable once AccessToModifiedClosure
             void onChange(ValueChangedEvent<WorkingBeatmap> valueChangedEvent) => changeCount++;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index a9747e73f9..9602758ffc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -92,10 +92,10 @@ namespace osu.Game.Tests.Visual.UserInterface
         [Test]
         public void TestExplicitConfig()
         {
-            AddStep("configure explicit content to allowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, true));
+            AddStep("configure explicit content to allowed", () => localConfig.SetValue(OsuSetting.ShowOnlineExplicitContent, true));
             AddAssert("explicit control set to show", () => control.ExplicitContent.Value == SearchExplicit.Show);
 
-            AddStep("configure explicit content to disallowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, false));
+            AddStep("configure explicit content to disallowed", () => localConfig.SetValue(OsuSetting.ShowOnlineExplicitContent, false));
             AddAssert("explicit control set to hide", () => control.ExplicitContent.Value == SearchExplicit.Hide);
         }
 
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs
index 45720548c8..493e2f54e5 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOnScreenDisplay.cs
@@ -44,22 +44,22 @@ namespace osu.Game.Tests.Visual.UserInterface
 
             protected override void InitialiseDefaults()
             {
-                Set(TestConfigSetting.ToggleSettingNoKeybind, false);
-                Set(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1);
-                Set(TestConfigSetting.ToggleSettingWithKeybind, false);
-                Set(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1);
+                SetDefault(TestConfigSetting.ToggleSettingNoKeybind, false);
+                SetDefault(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1);
+                SetDefault(TestConfigSetting.ToggleSettingWithKeybind, false);
+                SetDefault(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1);
 
                 base.InitialiseDefaults();
             }
 
-            public void ToggleSetting(TestConfigSetting setting) => Set(setting, !Get<bool>(setting));
+            public void ToggleSetting(TestConfigSetting setting) => SetValue(setting, !Get<bool>(setting));
 
             public void IncrementEnumSetting(TestConfigSetting setting)
             {
                 var nextValue = Get<EnumSetting>(setting) + 1;
                 if (nextValue > EnumSetting.Setting4)
                     nextValue = EnumSetting.Setting1;
-                Set(setting, nextValue);
+                SetValue(setting, nextValue);
             }
 
             public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
index 567d9f0d62..46c3b8bc3b 100644
--- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
                 storage.DeleteDirectory(string.Empty);
 
                 using (var storageConfig = new TournamentStorageManager(storage))
-                    storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
+                    storageConfig.SetValue(StorageConfig.CurrentTournament, custom_tournament);
 
                 try
                 {
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
index 2ba1b6be8f..5d9fed6288 100644
--- a/osu.Game.Tournament/IO/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentStorage.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Tournament.IO
             moveFileIfExists("drawings.ini", destination);
 
             ChangeTargetStorage(newStorage);
-            storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
+            storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament);
             storageConfig.Save();
         }
 
diff --git a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
index d197c0f5d9..1a2f5a1ff4 100644
--- a/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/DrawingsConfigManager.cs
@@ -12,8 +12,8 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
 
         protected override void InitialiseDefaults()
         {
-            Set(DrawingsConfig.Groups, 8, 1, 8);
-            Set(DrawingsConfig.TeamsPerGroup, 8, 1, 8);
+            SetDefault(DrawingsConfig.Groups, 8, 1, 8);
+            SetDefault(DrawingsConfig.TeamsPerGroup, 8, 1, 8);
         }
 
         public DrawingsConfigManager(Storage storage)
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index d0fa45bb7a..387cfbb193 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -24,126 +24,126 @@ namespace osu.Game.Configuration
         protected override void InitialiseDefaults()
         {
             // UI/selection defaults
-            Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
-            Set(OsuSetting.Skin, 0, -1, int.MaxValue);
+            SetDefault(OsuSetting.Ruleset, 0, 0, int.MaxValue);
+            SetDefault(OsuSetting.Skin, 0, -1, int.MaxValue);
 
-            Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
-            Set(OsuSetting.BeatmapDetailModsFilter, false);
+            SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
+            SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
 
-            Set(OsuSetting.ShowConvertedBeatmaps, true);
-            Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
-            Set(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1);
+            SetDefault(OsuSetting.ShowConvertedBeatmaps, true);
+            SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
+            SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1);
 
-            Set(OsuSetting.SongSelectGroupingMode, GroupMode.All);
-            Set(OsuSetting.SongSelectSortingMode, SortMode.Title);
+            SetDefault(OsuSetting.SongSelectGroupingMode, GroupMode.All);
+            SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title);
 
-            Set(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
+            SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
 
-            Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
+            SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
 
             // Online settings
-            Set(OsuSetting.Username, string.Empty);
-            Set(OsuSetting.Token, string.Empty);
+            SetDefault(OsuSetting.Username, string.Empty);
+            SetDefault(OsuSetting.Token, string.Empty);
 
-            Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
+            SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
 
-            Set(OsuSetting.SavePassword, false).ValueChanged += enabled =>
+            SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
             {
-                if (enabled.NewValue) Set(OsuSetting.SaveUsername, true);
+                if (enabled.NewValue) SetValue(OsuSetting.SaveUsername, true);
             };
 
-            Set(OsuSetting.SaveUsername, true).ValueChanged += enabled =>
+            SetDefault(OsuSetting.SaveUsername, true).ValueChanged += enabled =>
             {
-                if (!enabled.NewValue) Set(OsuSetting.SavePassword, false);
+                if (!enabled.NewValue) SetValue(OsuSetting.SavePassword, false);
             };
 
-            Set(OsuSetting.ExternalLinkWarning, true);
-            Set(OsuSetting.PreferNoVideo, false);
+            SetDefault(OsuSetting.ExternalLinkWarning, true);
+            SetDefault(OsuSetting.PreferNoVideo, false);
 
-            Set(OsuSetting.ShowOnlineExplicitContent, false);
+            SetDefault(OsuSetting.ShowOnlineExplicitContent, false);
 
             // Audio
-            Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
+            SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
 
-            Set(OsuSetting.MenuVoice, true);
-            Set(OsuSetting.MenuMusic, true);
+            SetDefault(OsuSetting.MenuVoice, true);
+            SetDefault(OsuSetting.MenuMusic, true);
 
-            Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1);
+            SetDefault(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1);
 
             // Input
-            Set(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
-            Set(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
-            Set(OsuSetting.AutoCursorSize, false);
+            SetDefault(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f);
+            SetDefault(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f);
+            SetDefault(OsuSetting.AutoCursorSize, false);
 
-            Set(OsuSetting.MouseDisableButtons, false);
-            Set(OsuSetting.MouseDisableWheel, false);
-            Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay);
+            SetDefault(OsuSetting.MouseDisableButtons, false);
+            SetDefault(OsuSetting.MouseDisableWheel, false);
+            SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay);
 
             // Graphics
-            Set(OsuSetting.ShowFpsDisplay, false);
+            SetDefault(OsuSetting.ShowFpsDisplay, false);
 
-            Set(OsuSetting.ShowStoryboard, true);
-            Set(OsuSetting.BeatmapSkins, true);
-            Set(OsuSetting.BeatmapColours, true);
-            Set(OsuSetting.BeatmapHitsounds, true);
+            SetDefault(OsuSetting.ShowStoryboard, true);
+            SetDefault(OsuSetting.BeatmapSkins, true);
+            SetDefault(OsuSetting.BeatmapColours, true);
+            SetDefault(OsuSetting.BeatmapHitsounds, true);
 
-            Set(OsuSetting.CursorRotation, true);
+            SetDefault(OsuSetting.CursorRotation, true);
 
-            Set(OsuSetting.MenuParallax, true);
+            SetDefault(OsuSetting.MenuParallax, true);
 
             // Gameplay
-            Set(OsuSetting.DimLevel, 0.8, 0, 1, 0.01);
-            Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
-            Set(OsuSetting.LightenDuringBreaks, true);
+            SetDefault(OsuSetting.DimLevel, 0.8, 0, 1, 0.01);
+            SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
+            SetDefault(OsuSetting.LightenDuringBreaks, true);
 
-            Set(OsuSetting.HitLighting, true);
+            SetDefault(OsuSetting.HitLighting, true);
 
-            Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
-            Set(OsuSetting.ShowProgressGraph, true);
-            Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
-            Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
-            Set(OsuSetting.KeyOverlay, false);
-            Set(OsuSetting.PositionalHitSounds, true);
-            Set(OsuSetting.AlwaysPlayFirstComboBreak, true);
-            Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
+            SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
+            SetDefault(OsuSetting.ShowProgressGraph, true);
+            SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
+            SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
+            SetDefault(OsuSetting.KeyOverlay, false);
+            SetDefault(OsuSetting.PositionalHitSounds, true);
+            SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
+            SetDefault(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
 
-            Set(OsuSetting.FloatingComments, false);
+            SetDefault(OsuSetting.FloatingComments, false);
 
-            Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
+            SetDefault(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
 
-            Set(OsuSetting.IncreaseFirstObjectVisibility, true);
-            Set(OsuSetting.GameplayDisableWinKey, true);
+            SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true);
+            SetDefault(OsuSetting.GameplayDisableWinKey, true);
 
             // Update
-            Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
+            SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
 
-            Set(OsuSetting.Version, string.Empty);
+            SetDefault(OsuSetting.Version, string.Empty);
 
-            Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg);
-            Set(OsuSetting.ScreenshotCaptureMenuCursor, false);
+            SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg);
+            SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false);
 
-            Set(OsuSetting.SongSelectRightMouseScroll, false);
+            SetDefault(OsuSetting.SongSelectRightMouseScroll, false);
 
-            Set(OsuSetting.Scaling, ScalingMode.Off);
+            SetDefault(OsuSetting.Scaling, ScalingMode.Off);
 
-            Set(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
-            Set(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
+            SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
+            SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
 
-            Set(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f);
-            Set(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f);
+            SetDefault(OsuSetting.ScalingPositionX, 0.5f, 0f, 1f);
+            SetDefault(OsuSetting.ScalingPositionY, 0.5f, 0f, 1f);
 
-            Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
+            SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
 
-            Set(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f);
+            SetDefault(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f);
 
-            Set(OsuSetting.IntroSequence, IntroSequence.Triangles);
+            SetDefault(OsuSetting.IntroSequence, IntroSequence.Triangles);
 
-            Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
-            Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
+            SetDefault(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
+            SetDefault(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
 
-            Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
+            SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
 
-            Set(OsuSetting.EditorWaveformOpacity, 1f);
+            SetDefault(OsuSetting.EditorWaveformOpacity, 1f);
         }
 
         public OsuConfigManager(Storage storage)
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index fd401119ff..36eb6964dd 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -14,10 +14,10 @@ namespace osu.Game.Configuration
     {
         protected override void InitialiseDefaults()
         {
-            Set(Static.LoginOverlayDisplayed, false);
-            Set(Static.MutedAudioNotificationShownOnce, false);
-            Set(Static.LastHoverSoundPlaybackTime, (double?)null);
-            Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
+            SetDefault(Static.LoginOverlayDisplayed, false);
+            SetDefault(Static.MutedAudioNotificationShownOnce, false);
+            SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
+            SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
         }
     }
 
diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs
index 929f8f22ad..90ea42b638 100644
--- a/osu.Game/Configuration/StorageConfigManager.cs
+++ b/osu.Game/Configuration/StorageConfigManager.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Configuration
         {
             base.InitialiseDefaults();
 
-            Set(StorageConfig.FullPath, string.Empty);
+            SetDefault(StorageConfig.FullPath, string.Empty);
         }
     }
 
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 8097f61ea4..7df5d820ee 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -58,7 +58,7 @@ namespace osu.Game.IO
         /// </summary>
         public void ResetCustomStoragePath()
         {
-            storageConfig.Set(StorageConfig.FullPath, string.Empty);
+            storageConfig.SetValue(StorageConfig.FullPath, string.Empty);
             storageConfig.Save();
 
             ChangeTargetStorage(defaultStorage);
@@ -103,7 +103,7 @@ namespace osu.Game.IO
         public override void Migrate(Storage newStorage)
         {
             base.Migrate(newStorage);
-            storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath("."));
+            storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath("."));
             storageConfig.Save();
         }
     }
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index ede64c0340..944525c119 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Online.API
             thread.Start();
         }
 
-        private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.Set(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
+        private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.SetValue(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
 
         internal new void Schedule(Action action) => base.Schedule(action);
 
@@ -134,7 +134,7 @@ namespace osu.Game.Online.API
                         state.Value = APIState.Connecting;
 
                         // save the username at this point, if the user requested for it to be.
-                        config.Set(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
+                        config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
 
                         if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password))
                         {
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index 9a0454bc95..1c72f3ebe2 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Updater
 
             // debug / local compilations will reset to a non-release string.
             // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations).
-            config.Set(OsuSetting.Version, version);
+            config.SetValue(OsuSetting.Version, version);
         }
 
         private readonly object updateTaskLock = new object();

From eda891223c469cdb23a07fa8faeac7331db2aa33 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 16:47:12 +0900
Subject: [PATCH 247/434] Start the editor with empty artist/creator/difficulty
 name fields

---
 osu.Game/Beatmaps/BeatmapManager.cs            | 3 ---
 osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++----
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index f42fba79cb..115d1b33bb 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -113,8 +113,6 @@ namespace osu.Game.Beatmaps
         {
             var metadata = new BeatmapMetadata
             {
-                Artist = "artist",
-                Title = "title",
                 Author = user,
             };
 
@@ -128,7 +126,6 @@ namespace osu.Game.Beatmaps
                         BaseDifficulty = new BeatmapDifficulty(),
                         Ruleset = ruleset,
                         Metadata = metadata,
-                        Version = "difficulty"
                     }
                 }
             };
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index e812c042fb..2b10be0423 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -28,25 +28,25 @@ namespace osu.Game.Screens.Edit.Setup
                 },
                 artistTextBox = new LabelledTextBox
                 {
-                    Label = "Artist",
+                    PlaceholderText = "Artist",
                     Current = { Value = Beatmap.Metadata.Artist },
                     TabbableContentContainer = this
                 },
                 titleTextBox = new LabelledTextBox
                 {
-                    Label = "Title",
+                    PlaceholderText = "Title",
                     Current = { Value = Beatmap.Metadata.Title },
                     TabbableContentContainer = this
                 },
                 creatorTextBox = new LabelledTextBox
                 {
-                    Label = "Creator",
+                    PlaceholderText = "Creator",
                     Current = { Value = Beatmap.Metadata.AuthorString },
                     TabbableContentContainer = this
                 },
                 difficultyTextBox = new LabelledTextBox
                 {
-                    Label = "Difficulty Name",
+                    PlaceholderText = "Difficulty Name",
                     Current = { Value = Beatmap.BeatmapInfo.Version },
                     TabbableContentContainer = this
                 },

From 26d6f96c4e2decdba1836a98280a2ba6381c379d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 16:56:58 +0900
Subject: [PATCH 248/434] Fix LabelledTextBox not correctly forwarding focus to
 its underlying TextBox component

---
 osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
index 4aeda74be8..266eb11319 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input.Events;
 using osu.Game.Graphics.UserInterface;
 
 namespace osu.Game.Graphics.UserInterfaceV2
@@ -53,6 +54,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
             CornerRadius = CORNER_RADIUS,
         };
 
+        public override bool AcceptsFocus => true;
+
+        protected override void OnFocus(FocusEvent e)
+        {
+            base.OnFocus(e);
+            GetContainingInputManager().ChangeFocus(Component);
+        }
+
         protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>
         {
             t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText);

From 5adc675862afb015ad737db47ebda917fc24c2f2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 16:57:14 +0900
Subject: [PATCH 249/434] Focus artist textbox on entering song setup if fields
 are empty

---
 osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 2b10be0423..c5a2b77ab4 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -56,6 +56,14 @@ namespace osu.Game.Screens.Edit.Setup
                 item.OnCommit += onCommit;
         }
 
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            if (string.IsNullOrEmpty(artistTextBox.Current.Value))
+                GetContainingInputManager().ChangeFocus(artistTextBox);
+        }
+
         private void onCommit(TextBox sender, bool newText)
         {
             if (!newText) return;

From 3b6a1180b68515b82a9e970f84a3e51d5f908f4e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 17:02:11 +0900
Subject: [PATCH 250/434] Remove non-accessed field

---
 osu.Game/Screens/Edit/Editor.cs | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 3a4c3491ff..0c24eb6a4d 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -74,7 +74,6 @@ namespace osu.Game.Screens.Edit
 
         private string lastSavedHash;
 
-        private Box bottomBackground;
         private Container<EditorScreen> screenContainer;
 
         private EditorScreen currentScreen;
@@ -242,7 +241,7 @@ namespace osu.Game.Screens.Edit
                         Height = 60,
                         Children = new Drawable[]
                         {
-                            bottomBackground = new Box
+                            new Box
                             {
                                 RelativeSizeAxes = Axes.Both,
                                 Colour = colours.Gray2

From d0e61e5b4d609c342e55e2b8a2ed86c66977d11e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 17:14:04 +0900
Subject: [PATCH 251/434] Put back the label

---
 osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index c5a2b77ab4..f429164ece 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -28,25 +28,25 @@ namespace osu.Game.Screens.Edit.Setup
                 },
                 artistTextBox = new LabelledTextBox
                 {
-                    PlaceholderText = "Artist",
+                    Label = "Artist",
                     Current = { Value = Beatmap.Metadata.Artist },
                     TabbableContentContainer = this
                 },
                 titleTextBox = new LabelledTextBox
                 {
-                    PlaceholderText = "Title",
+                    Label = "Title",
                     Current = { Value = Beatmap.Metadata.Title },
                     TabbableContentContainer = this
                 },
                 creatorTextBox = new LabelledTextBox
                 {
-                    PlaceholderText = "Creator",
+                    Label = "Creator",
                     Current = { Value = Beatmap.Metadata.AuthorString },
                     TabbableContentContainer = this
                 },
                 difficultyTextBox = new LabelledTextBox
                 {
-                    PlaceholderText = "Difficulty Name",
+                    Label = "Difficulty Name",
                     Current = { Value = Beatmap.BeatmapInfo.Version },
                     TabbableContentContainer = this
                 },

From a1a0074c3203e4f259fde9f87f9516acca239cf2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 18:05:11 +0900
Subject: [PATCH 252/434] Revert "Local framework"

This reverts commit b9b095ee75c960287a8636d06c07afddade6865f.
---
 osu.Desktop.slnf         |  6 ++----
 osu.Game/osu.Game.csproj |  4 +---
 osu.iOS.props            |  4 ++--
 osu.sln                  | 42 ----------------------------------------
 4 files changed, 5 insertions(+), 51 deletions(-)

diff --git a/osu.Desktop.slnf b/osu.Desktop.slnf
index 1e41d0af0e..d2c14d321a 100644
--- a/osu.Desktop.slnf
+++ b/osu.Desktop.slnf
@@ -15,9 +15,7 @@
       "osu.Game.Tests\\osu.Game.Tests.csproj",
       "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
       "osu.Game.Tournament\\osu.Game.Tournament.csproj",
-      "osu.Game\\osu.Game.csproj",
-      "../osu-framework/osu.Framework/osu.Framework.csproj",
-      "../osu-framework/osu.Framework/osu.Framework.NativeLibs.csproj"
+      "osu.Game\\osu.Game.csproj"
     ]
   }
-}
+}
\ No newline at end of file
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f2fc1726cd..90c8b98f42 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,13 +29,11 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
   </ItemGroup>
-  <ItemGroup>
-    <ProjectReference Include="..\..\osu-framework\osu.Framework\osu.Framework.csproj" />
-  </ItemGroup>
 </Project>
diff --git a/osu.iOS.props b/osu.iOS.props
index 30df8c423e..ccd33bf88c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <ProjectReference Include="../../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.309.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -93,7 +93,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <ProjectReference Include="../../osu-framework/osu.Framework/osu.Framework.csproj" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />
diff --git a/osu.sln b/osu.sln
index 4d0b3656e7..c9453359b1 100644
--- a/osu.sln
+++ b/osu.sln
@@ -66,12 +66,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Benchmarks", "osu.Game.Benchmarks\osu.Game.Benchmarks.csproj", "{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework", "..\osu-framework\osu.Framework\osu.Framework.csproj", "{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.iOS", "..\osu-framework\osu.Framework.iOS\osu.Framework.iOS.csproj", "{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.NativeLibs", "..\osu-framework\osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj", "{500039B3-0706-40C3-B6E7-1FD9187644A5}"
-EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -418,42 +412,6 @@ Global
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhone.Build.0 = Release|Any CPU
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{93632F2D-2BB4-46C1-A7B8-F8CF2FB27118}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhone.Build.0 = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|Any CPU.Build.0 = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhone.ActiveCfg = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhone.Build.0 = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{7EBA330C-6DD9-4F30-9332-6542D86D5BE1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhone.ActiveCfg = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhone.Build.0 = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|Any CPU.Build.0 = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhone.ActiveCfg = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhone.Build.0 = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{7A6EEFF0-760C-4EE5-BB5E-101E7D013392}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhone.ActiveCfg = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhone.Build.0 = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|Any CPU.Build.0 = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhone.ActiveCfg = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhone.Build.0 = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
-		{500039B3-0706-40C3-B6E7-1FD9187644A5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

From 577d40d8d155daf4af76278b7dce81df355eef59 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 18:05:18 +0900
Subject: [PATCH 253/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 5b700224db..e0392bd687 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.309.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.317.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index fa1b0a95c3..360c522193 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 71fcdd45f3..b763a91dfb 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.309.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.317.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -91,7 +91,7 @@
     <PackageReference Include="DiffPlex" Version="1.6.3" />
     <PackageReference Include="Humanizer" Version="2.8.26" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.309.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 79041c1c4b7a386781a4c01cd2d56a1e74f601eb Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 18:07:42 +0900
Subject: [PATCH 254/434] Remove osuTK reference

---
 osu.Desktop/Program.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 0c527ba881..d06c4b6746 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -22,7 +22,6 @@ namespace osu.Desktop
         {
             // Back up the cwd before DesktopGameHost changes it
             var cwd = Environment.CurrentDirectory;
-            bool useOsuTK = args.Contains("--tk");
 
             using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
             {

From fccd495f27e3955f33b4a2f3fd5258fffd7b5faa Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 18:07:51 +0900
Subject: [PATCH 255/434] Remove obsoleted setting for now

---
 osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 036c4edfba..fb908a7669 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -53,11 +53,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     LabelText = "Cursor sensitivity",
                     Current = localSensitivity
                 },
-                new SettingsCheckbox
-                {
-                    LabelText = "Map absolute input to window",
-                    Current = config.GetBindable<bool>(FrameworkSetting.MapAbsoluteInputToWindow)
-                },
                 confineMouseModeSetting = new SettingsEnumDropdown<OsuConfineMouseMode>
                 {
                     LabelText = "Confine mouse cursor to window",

From 4bf57ad86080133e9d437a0079eb3255c8e9c789 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 18:24:24 +0900
Subject: [PATCH 256/434] Remove remaining reference to obsolete value

---
 osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
index 6ca7707906..bf5338d81a 100644
--- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
+++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Platform;
@@ -62,10 +61,6 @@ namespace osu.Game.Tests.Visual.Navigation
 
                 RecycleLocalStorage();
 
-                // see MouseSettings
-                var frameworkConfig = host.Dependencies.Get<FrameworkConfigManager>();
-                frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity).Disabled = false;
-
                 CreateGame();
             });
 

From 8046b5a818f8d2a69d7199e6cff6fa2a1db024d8 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Wed, 17 Mar 2021 17:35:49 +0800
Subject: [PATCH 257/434] set text to platform clipboard on copy

---
 osu.Game/Screens/Edit/Editor.cs | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 0ba202b082..88383bd3ed 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -21,6 +21,7 @@ using osu.Framework.Screens;
 using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
+using osu.Game.Extensions;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Graphics.UserInterface;
@@ -29,6 +30,7 @@ using osu.Game.IO.Serialization;
 using osu.Game.Online.API;
 using osu.Game.Overlays;
 using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Screens.Edit.Components;
 using osu.Game.Screens.Edit.Components.Menus;
 using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -60,6 +62,9 @@ namespace osu.Game.Screens.Edit
 
         protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash;
 
+        [Resolved]
+        private GameHost host { get; set; }
+
         [Resolved]
         private BeatmapManager beatmapManager { get; set; }
 
@@ -104,7 +109,7 @@ namespace osu.Game.Screens.Edit
         private MusicController music { get; set; }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, GameHost host, OsuConfigManager config)
+        private void load(OsuColour colours, OsuConfigManager config)
         {
             if (Beatmap.Value is DummyWorkingBeatmap)
             {
@@ -542,8 +547,12 @@ namespace osu.Game.Screens.Edit
         protected void Copy()
         {
             if (editorBeatmap.SelectedHitObjects.Count == 0)
+            {
+                host.GetClipboard()?.SetText($"{clock.CurrentTime.ToEditorFormattedString()} - ");
                 return;
+            }
 
+            host.GetClipboard()?.SetText($"{editorBeatmap.SelectedHitObjects.FirstOrDefault().StartTime.ToEditorFormattedString()} ({string.Join(',', editorBeatmap.SelectedHitObjects.Select(h => ((h as IHasComboInformation)?.IndexInCurrentCombo + 1 ?? 0).ToString()))}) - ");
             clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
         }
 

From f7ec79c5f42cd90f3201ae8855bc4d53fc5594fc Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 17 Mar 2021 19:02:25 +0900
Subject: [PATCH 258/434] Fix incorrect generic type

---
 osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
index 768f2057a2..c1c968e862 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation
         {
             AddAssert("config has migrated value", () => Precision.AlmostEquals(Game.LocalConfig.Get<double>(OsuSetting.DisplayStarsMaximum), 10.1));
 
-            AddStep("set value again", () => Game.LocalConfig.SetValue(OsuSetting.DisplayStarsMaximum, 10));
+            AddStep("set value again", () => Game.LocalConfig.SetValue(OsuSetting.DisplayStarsMaximum, 10.0));
 
             AddStep("force save config", () => Game.LocalConfig.Save());
 

From 133ff085a53711229834c0c3b3944f1c29d91b73 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Wed, 17 Mar 2021 18:06:40 +0800
Subject: [PATCH 259/434] refactor code

---
 osu.Game/Screens/Edit/Editor.cs | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 88383bd3ed..fdb31a8b8c 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using System.Text;
 using osu.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -546,14 +547,24 @@ namespace osu.Game.Screens.Edit
 
         protected void Copy()
         {
+            var builder = new StringBuilder();
+            const string suffix = " - ";
+
             if (editorBeatmap.SelectedHitObjects.Count == 0)
             {
-                host.GetClipboard()?.SetText($"{clock.CurrentTime.ToEditorFormattedString()} - ");
-                return;
+                builder.Append(clock.CurrentTime.ToEditorFormattedString());
+            }
+            else
+            {
+                var orderedHitObjects = editorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime);
+                builder.Append(orderedHitObjects.FirstOrDefault().StartTime.ToEditorFormattedString());
+                builder.Append($" ({string.Join(',', orderedHitObjects.Cast<IHasComboInformation>().Select(h => h.IndexInCurrentCombo + 1))})");
+
+                clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
             }
 
-            host.GetClipboard()?.SetText($"{editorBeatmap.SelectedHitObjects.FirstOrDefault().StartTime.ToEditorFormattedString()} ({string.Join(',', editorBeatmap.SelectedHitObjects.Select(h => ((h as IHasComboInformation)?.IndexInCurrentCombo + 1 ?? 0).ToString()))}) - ");
-            clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
+            builder.Append(suffix);
+            host.GetClipboard()?.SetText(builder.ToString());
         }
 
         protected void Paste()

From e59b8b4ce66a6b30057add268d1620d5af8be231 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Wed, 17 Mar 2021 19:07:29 +0900
Subject: [PATCH 260/434] Fix test checking nullable string

---
 .../Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 2f558a6379..591095252f 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
             }));
 
             AddAssert("mapped by text not present", () =>
-                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Current.Value, "mapped", "by")));
+                this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
         }
 
         private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score);

From 51e0304c54a604bab6d6c8007c59ec755b115b2d Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Wed, 17 Mar 2021 18:31:09 +0800
Subject: [PATCH 261/434] properly format strings per ruleset

---
 osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 2 ++
 osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs | 2 ++
 osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs     | 2 ++
 osu.Game/Rulesets/Objects/HitObject.cs            | 2 ++
 osu.Game/Screens/Edit/Editor.cs                   | 2 +-
 5 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index ae45182960..631b50d686 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -120,5 +120,7 @@ namespace osu.Game.Rulesets.Catch.Objects
         }
 
         protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+        public override string ToEditorString() => (IndexInCurrentCombo + 1).ToString();
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 27bf50493d..c43d223335 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Objects
 
         protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
 
+        public override string ToEditorString() => $"{StartTime}|{Column}";
+
         #region LegacyBeatmapEncoder
 
         float IHasXPosition.X => Column;
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 22b64af3df..e784d13084 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -130,5 +130,7 @@ namespace osu.Game.Rulesets.Osu.Objects
         }
 
         protected override HitWindows CreateHitWindows() => new OsuHitWindows();
+
+        public override string ToEditorString() => (IndexInCurrentCombo + 1).ToString();
     }
 }
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index 826d411822..fa7b2811cc 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -168,6 +168,8 @@ namespace osu.Game.Rulesets.Objects
         /// </summary>
         [NotNull]
         protected virtual HitWindows CreateHitWindows() => new HitWindows();
+
+        public virtual string ToEditorString() => string.Empty;
     }
 
     public static class HitObjectExtensions
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index fdb31a8b8c..a6e84d59a7 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -558,7 +558,7 @@ namespace osu.Game.Screens.Edit
             {
                 var orderedHitObjects = editorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime);
                 builder.Append(orderedHitObjects.FirstOrDefault().StartTime.ToEditorFormattedString());
-                builder.Append($" ({string.Join(',', orderedHitObjects.Cast<IHasComboInformation>().Select(h => h.IndexInCurrentCombo + 1))})");
+                builder.Append($" ({string.Join(',', orderedHitObjects.Select(h => h.ToEditorString()))})");
 
                 clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
             }

From 3bfde7341f25b9fb06213426fed89a9e5accda0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 17:14:53 +0100
Subject: [PATCH 262/434] Revert "Remove unnecessary overrides"

This reverts commit f4e508b57051e887a00a8f4649e4616b762a8c8c.
---
 osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs             | 2 ++
 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index 1b5d576c1e..69b81d6d5c 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Mania.UI
     {
         private const float default_large_faint_size = 0.8f;
 
+        public override bool RemoveWhenNotAlive => true;
+
         [Resolved]
         private Column column { get; set; }
 
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index bef9279bac..aad9f53b93 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
 {
     public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion
     {
+        public override bool RemoveWhenNotAlive => false;
+
         private readonly Drawable sprite;
 
         [CanBeNull]

From f1e66cc420ce69e133f08068fdc2c1affd3e02cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 18:37:11 +0100
Subject: [PATCH 263/434] Adjust test namespace

---
 osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index a824696022..aafd0ee32b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -7,7 +7,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Screens.Edit.Components;
 using osuTK;
 
-namespace osu.Game.Tests.Visual.Editor
+namespace osu.Game.Tests.Visual.Editing
 {
     [TestFixture]
     public class TestSceneEditorClock : EditorClockTestScene

From 3b55eeb416c01f1f2ba5d2fa0a8ce0d7a25d6bd7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 18:39:48 +0100
Subject: [PATCH 264/434] Fix test failure by setting beatmap

Post-merge, it was failing because somewhere along the way
`EditorClockTestScene` started expecting inheritors to set the beatmap
themselves explicitly in BDL.
---
 osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index aafd0ee32b..63d7dbc2b5 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -2,8 +2,10 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using NUnit.Framework;
+using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Edit.Components;
 using osuTK;
 
@@ -35,6 +37,12 @@ namespace osu.Game.Tests.Visual.Editing
             });
         }
 
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+        }
+
         [Test]
         public void TestStopAtTrackEnd()
         {

From 21e18c9f6eae256c894ec81ff00dc529dc7762fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 18:44:21 +0100
Subject: [PATCH 265/434] Fix test hangs in browser due to changing tracks via
 music controller

---
 osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index 63d7dbc2b5..58375f295b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -41,6 +41,9 @@ namespace osu.Game.Tests.Visual.Editing
         private void load()
         {
             Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+            // ensure that music controller does not change this beatmap due to it
+            // completing naturally as part of the test.
+            Beatmap.Disabled = true;
         }
 
         [Test]

From 6cea74f0fada8f90bf27e3005cccd2ca5dc42706 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Wed, 17 Mar 2021 13:13:13 -0700
Subject: [PATCH 266/434] Remove available kudosu section from user profile
 overlay in line with web

---
 .../Profile/Sections/Kudosu/KudosuInfo.cs     | 34 ++-----------------
 1 file changed, 3 insertions(+), 31 deletions(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index d4d0976724..e5b4193f3b 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -23,44 +23,17 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
         {
             this.user.BindTo(user);
             CountSection total;
-            CountSection avaliable;
             RelativeSizeAxes = Axes.X;
             AutoSizeAxes = Axes.Y;
             Masking = true;
             CornerRadius = 3;
-            Children = new Drawable[]
-            {
-                new FillFlowContainer
-                {
-                    RelativeSizeAxes = Axes.X,
-                    AutoSizeAxes = Axes.Y,
-                    Direction = FillDirection.Horizontal,
-                    Spacing = new Vector2(5, 0),
-                    Children = new[]
-                    {
-                        total = new CountTotal(),
-                        avaliable = new CountAvailable()
-                    }
-                }
-            };
-            this.user.ValueChanged += u =>
-            {
-                total.Count = u.NewValue?.Kudosu.Total ?? 0;
-                avaliable.Count = u.NewValue?.Kudosu.Available ?? 0;
-            };
+            Child = total = new CountTotal();
+
+            this.user.ValueChanged += u => total.Count = u.NewValue?.Kudosu.Total ?? 0;
         }
 
         protected override bool OnClick(ClickEvent e) => true;
 
-        private class CountAvailable : CountSection
-        {
-            public CountAvailable()
-                : base("Kudosu Avaliable")
-            {
-                DescriptionText.Text = "Kudosu can be traded for kudosu stars, which will help your beatmap get more attention. This is the number of kudosu you haven't traded in yet.";
-            }
-        }
-
         private class CountTotal : CountSection
         {
             public CountTotal()
@@ -86,7 +59,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             public CountSection(string header)
             {
                 RelativeSizeAxes = Axes.X;
-                Width = 0.5f;
                 AutoSizeAxes = Axes.Y;
                 Padding = new MarginPadding { Top = 10, Bottom = 20 };
                 Child = new FillFlowContainer

From 599c55fca5d08f0f2a30cde6be78273b6ad24404 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Wed, 17 Mar 2021 13:14:18 -0700
Subject: [PATCH 267/434] Update total kudosu earned description text in line
 with web

---
 osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index e5b4193f3b..87622939e2 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
                 : base("Total Kudosu Earned")
             {
                 DescriptionText.AddText("Based on how much of a contribution the user has made to beatmap moderation. See ");
-                DescriptionText.AddLink("this link", "https://osu.ppy.sh/wiki/Kudosu");
+                DescriptionText.AddLink("this page", "https://osu.ppy.sh/wiki/Kudosu");
                 DescriptionText.AddText(" for more information.");
             }
         }

From 2e63c2ce20ef4c707150f2eaf3c5a640a9c963c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 21:50:45 +0100
Subject: [PATCH 268/434] Fix selection box operation hotkeys not registering
 in change handler

Could lead to crashes after reversing a note cluster and playing it
back.

The root cause of the crash was that the hotkey operations were not ran
inside of an editor change handler operation. This, in turn, caused the
autoplay replay to not be regenerated after flipping an object cluster,
therefore finally manifesting as a hard crash due to negative time
offsets appearing in judgement results, which interfered with the
default implementation of note lock.

Note that this incidentally also fixes the fact that selection box
hotkey operations (reverse and flip) did not handle undo/redo.
---
 .../Edit/Compose/Components/SelectionBox.cs       | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
index 2f4721f63e..9d6b44e207 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
@@ -113,16 +113,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
             if (e.Repeat || !e.ControlPressed)
                 return false;
 
+            bool runOperationFromHotkey(Func<bool> operation)
+            {
+                operationStarted();
+                bool result = operation?.Invoke() ?? false;
+                operationEnded();
+
+                return result;
+            }
+
             switch (e.Key)
             {
                 case Key.G:
-                    return CanReverse && OnReverse?.Invoke() == true;
+                    return CanReverse && runOperationFromHotkey(OnReverse);
 
                 case Key.H:
-                    return CanScaleX && OnFlip?.Invoke(Direction.Horizontal) == true;
+                    return CanScaleX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false);
 
                 case Key.J:
-                    return CanScaleY && OnFlip?.Invoke(Direction.Vertical) == true;
+                    return CanScaleY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false);
             }
 
             return base.OnKeyDown(e);

From 08ffe425f9b9a1cd9eba6c3cd903264680244090 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Wed, 17 Mar 2021 14:46:23 -0700
Subject: [PATCH 269/434] Update kudosu description color in line with web

---
 osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index 87622939e2..115d705766 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -103,7 +103,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
             private void load(OverlayColourProvider colourProvider)
             {
                 lineBackground.Colour = colourProvider.Highlight1;
-                DescriptionText.Colour = colourProvider.Foreground1;
             }
         }
     }

From f95ce90c95495691e1d7c57e10a8c456ffbfc82c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Wed, 17 Mar 2021 23:32:08 +0100
Subject: [PATCH 270/434] Adjust kudosu count formatting

---
 osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index 115d705766..cdb24b784c 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
 
             public new int Count
             {
-                set => valueText.Text = value.ToString();
+                set => valueText.Text = value.ToString("N0");
             }
 
             public CountSection(string header)

From df6570ebf544dd6157e14376055ec271fd9c132c Mon Sep 17 00:00:00 2001
From: voidedWarranties <wz68.scratch@gmail.com>
Date: Wed, 17 Mar 2021 15:31:16 -0700
Subject: [PATCH 271/434] Improve logic and add previously failing test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
---
 .../Visual/Editing/TestSceneEditorClock.cs    | 35 ++++++++++++++-----
 osu.Game/Screens/Edit/EditorClock.cs          | 26 +++++++-------
 2 files changed, 39 insertions(+), 22 deletions(-)

diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
index 58375f295b..390198be04 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -49,14 +49,33 @@ namespace osu.Game.Tests.Visual.Editing
         [Test]
         public void TestStopAtTrackEnd()
         {
-            AddStep("Reset clock", () => Clock.Seek(0));
-            AddStep("Start clock", Clock.Start);
-            AddAssert("Clock running", () => Clock.IsRunning);
-            AddStep("Seek near end", () => Clock.Seek(Clock.TrackLength - 250));
-            AddUntilStep("Clock stops", () => !Clock.IsRunning);
-            AddAssert("Clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
-            AddStep("Start clock again", Clock.Start);
-            AddAssert("Clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
+            AddStep("reset clock", () => Clock.Seek(0));
+
+            AddStep("start clock", Clock.Start);
+            AddAssert("clock running", () => Clock.IsRunning);
+
+            AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250));
+            AddUntilStep("clock stops", () => !Clock.IsRunning);
+
+            AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
+
+            AddStep("start clock again", Clock.Start);
+            AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
+        }
+
+        [Test]
+        public void TestWrapWhenStoppedAtTrackEnd()
+        {
+            AddStep("reset clock", () => Clock.Seek(0));
+
+            AddStep("stop clock", Clock.Stop);
+            AddAssert("clock stopped", () => !Clock.IsRunning);
+
+            AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength));
+            AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
+
+            AddStep("start clock again", Clock.Start);
+            AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
         }
     }
 }
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 94bb4f5228..e227bd29bf 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -172,6 +172,10 @@ namespace osu.Game.Screens.Edit
         public void Start()
         {
             ClearTransforms();
+
+            if (playbackFinished)
+                underlyingClock.Seek(0);
+
             underlyingClock.Start();
         }
 
@@ -222,21 +226,15 @@ namespace osu.Game.Screens.Edit
         {
             underlyingClock.ProcessFrame();
 
-            if (IsRunning)
-            {
-                var playbackAlreadyStopped = playbackFinished;
-                playbackFinished = CurrentTime >= TrackLength;
+            var playbackAlreadyStopped = playbackFinished;
+            playbackFinished = CurrentTime >= TrackLength;
 
-                if (playbackFinished)
-                {
-                    if (!playbackAlreadyStopped)
-                    {
-                        underlyingClock.Stop();
-                        underlyingClock.Seek(TrackLength);
-                    }
-                    else
-                        underlyingClock.Seek(0);
-                }
+            if (playbackFinished && !playbackAlreadyStopped)
+            {
+                if (IsRunning)
+                    underlyingClock.Stop();
+
+                underlyingClock.Seek(TrackLength);
             }
         }
 

From bb3c3f302aa4b0cd2f0e5e8a9d80235fc7d810fd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Mar 2021 15:36:07 +0900
Subject: [PATCH 272/434] Fix skin parser not stripping whitespace before
 parsing

---
 osu.Game.Tests/Resources/skin-with-space.ini  | 2 ++
 osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 9 +++++++++
 osu.Game/Beatmaps/Formats/LegacyDecoder.cs    | 2 ++
 3 files changed, 13 insertions(+)
 create mode 100644 osu.Game.Tests/Resources/skin-with-space.ini

diff --git a/osu.Game.Tests/Resources/skin-with-space.ini b/osu.Game.Tests/Resources/skin-with-space.ini
new file mode 100644
index 0000000000..3e64257a3e
--- /dev/null
+++ b/osu.Game.Tests/Resources/skin-with-space.ini
@@ -0,0 +1,2 @@
+[General] 
+Version: 2
\ No newline at end of file
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index aedf26ee75..dcb866c99f 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -91,6 +91,15 @@ namespace osu.Game.Tests.Skins
                 Assert.AreEqual(2.0m, decoder.Decode(stream).LegacyVersion);
         }
 
+        [Test]
+        public void TestStripWhitespace()
+        {
+            var decoder = new LegacySkinDecoder();
+            using (var resStream = TestResources.OpenResource("skin-with-space.ini"))
+            using (var stream = new LineBufferedReader(resStream))
+                Assert.AreEqual(2.0m, decoder.Decode(stream).LegacyVersion);
+        }
+
         [Test]
         public void TestDecodeLatestVersion()
         {
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 2fb24c24e0..bd1b6627b4 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -36,6 +36,8 @@ namespace osu.Game.Beatmaps.Formats
                 if (ShouldSkipLine(line))
                     continue;
 
+                line = line.Trim();
+
                 if (line.StartsWith('[') && line.EndsWith(']'))
                 {
                     if (!Enum.TryParse(line[1..^1], out section))

From 5b0d75ee56690dbb3a121d741768515417f51ee8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Mar 2021 16:30:30 +0900
Subject: [PATCH 273/434] Only trim trailing spaces to avoid breakage in
 storyboard parsing

---
 osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs  | 14 ++++++--------
 osu.Game/Beatmaps/Formats/LegacyDecoder.cs         |  4 +---
 .../Beatmaps/Formats/LegacyStoryboardDecoder.cs    |  2 --
 osu.Game/Skinning/LegacyManiaSkinDecoder.cs        |  2 --
 4 files changed, 7 insertions(+), 15 deletions(-)

diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 99dffa7041..40bc75e847 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -67,16 +67,14 @@ namespace osu.Game.Beatmaps.Formats
 
         protected override void ParseLine(Beatmap beatmap, Section section, string line)
         {
-            var strippedLine = StripComments(line);
-
             switch (section)
             {
                 case Section.General:
-                    handleGeneral(strippedLine);
+                    handleGeneral(line);
                     return;
 
                 case Section.Editor:
-                    handleEditor(strippedLine);
+                    handleEditor(line);
                     return;
 
                 case Section.Metadata:
@@ -84,19 +82,19 @@ namespace osu.Game.Beatmaps.Formats
                     return;
 
                 case Section.Difficulty:
-                    handleDifficulty(strippedLine);
+                    handleDifficulty(line);
                     return;
 
                 case Section.Events:
-                    handleEvent(strippedLine);
+                    handleEvent(line);
                     return;
 
                 case Section.TimingPoints:
-                    handleTimingPoint(strippedLine);
+                    handleTimingPoint(line);
                     return;
 
                 case Section.HitObjects:
-                    handleHitObject(strippedLine);
+                    handleHitObject(line);
                     return;
             }
 
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index bd1b6627b4..10a716963e 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps.Formats
                 if (ShouldSkipLine(line))
                     continue;
 
-                line = line.Trim();
+                line = StripComments(line).TrimEnd();
 
                 if (line.StartsWith('[') && line.EndsWith(']'))
                 {
@@ -73,8 +73,6 @@ namespace osu.Game.Beatmaps.Formats
 
         protected virtual void ParseLine(T output, Section section, string line)
         {
-            line = StripComments(line);
-
             switch (section)
             {
                 case Section.Colours:
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index b9bf6823b5..6301c42deb 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -45,8 +45,6 @@ namespace osu.Game.Beatmaps.Formats
 
         protected override void ParseLine(Storyboard storyboard, Section section, string line)
         {
-            line = StripComments(line);
-
             switch (section)
             {
                 case Section.General:
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index 0a1de461ea..5308640bdd 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
@@ -31,8 +31,6 @@ namespace osu.Game.Skinning
 
         protected override void ParseLine(List<LegacyManiaSkinConfiguration> output, Section section, string line)
         {
-            line = StripComments(line);
-
             switch (section)
             {
                 case Section.Mania:

From b68dc686ee9bf7c978fd746248403a6decbf18a4 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 18 Mar 2021 19:19:53 +0900
Subject: [PATCH 274/434] Fix converted mania scores not accounting for GREATs

---
 osu.Game/Scoring/ScoreManager.cs | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 1e90ee1ac7..5fa971ce80 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -20,6 +20,7 @@ using osu.Game.IO.Archives;
 using osu.Game.Online.API;
 using osu.Game.Online.API.Requests;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring.Legacy;
 
@@ -157,9 +158,19 @@ namespace osu.Game.Scoring
                 }
 
                 int beatmapMaxCombo;
+                double accuracy = score.Accuracy;
 
                 if (score.IsLegacyScore)
                 {
+                    if (score.RulesetID == 3)
+                    {
+                        // Recalculate mania's accuracy based on hit statistics.
+                        double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
+                        double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
+                        if (maxBaseScore > 0)
+                            accuracy = baseScore / maxBaseScore;
+                    }
+
                     // This score is guaranteed to be an osu!stable score.
                     // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
                     if (score.Beatmap.MaxCombo == null)
@@ -176,7 +187,7 @@ namespace osu.Game.Scoring
                         difficultyBindable.BindValueChanged(d =>
                         {
                             if (d.NewValue is StarDifficulty diff)
-                                updateScore(diff.MaxCombo);
+                                updateScore(diff.MaxCombo, accuracy);
                         }, true);
 
                         return;
@@ -191,10 +202,10 @@ namespace osu.Game.Scoring
                     beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
                 }
 
-                updateScore(beatmapMaxCombo);
+                updateScore(beatmapMaxCombo, accuracy);
             }
 
-            private void updateScore(int beatmapMaxCombo)
+            private void updateScore(int beatmapMaxCombo, double accuracy)
             {
                 if (beatmapMaxCombo == 0)
                 {
@@ -207,7 +218,7 @@ namespace osu.Game.Scoring
 
                 scoreProcessor.Mods.Value = score.Mods;
 
-                Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
+                Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
             }
         }
 

From 917717686a6bef391d86cb4a92683e9442ac8c78 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 18 Mar 2021 19:26:29 +0900
Subject: [PATCH 275/434] Expand explanatory comment

---
 osu.Game/Scoring/ScoreManager.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 5fa971ce80..7d0abc5996 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -164,7 +164,9 @@ namespace osu.Game.Scoring
                 {
                     if (score.RulesetID == 3)
                     {
-                        // Recalculate mania's accuracy based on hit statistics.
+                        // In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
+                        // To get around this, recalculate accuracy based on the hit statistics.
+                        // Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
                         double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
                         double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
                         if (maxBaseScore > 0)

From 0c3c8141dac75fbc7ccd5c8d79746dc413c8df21 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Thu, 18 Mar 2021 19:39:42 +0900
Subject: [PATCH 276/434] Remove Expires and RemoveWhenNotAlive override

---
 osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs | 4 ----
 osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs             | 4 ----
 2 files changed, 8 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index aad9f53b93..21bd35ad22 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -13,8 +13,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
 {
     public class LegacyHitExplosion : CompositeDrawable, IAnimatableHitExplosion
     {
-        public override bool RemoveWhenNotAlive => false;
-
         private readonly Drawable sprite;
 
         [CanBeNull]
@@ -73,8 +71,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
                 .Then().ScaleTo(1.1f, animation_time * 0.8)
                 .Then().ScaleTo(0.9f, animation_time * 0.4)
                 .Then().ScaleTo(1f, animation_time * 0.2);
-
-            Expire(true);
         }
 
         public void AnimateSecondHit()
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
index 5bb463353d..91e844187a 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
@@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Taiko.UI
 {
     internal class DefaultHitExplosion : CircularContainer, IAnimatableHitExplosion
     {
-        public override bool RemoveWhenNotAlive => false;
-
         private readonly HitResult result;
 
         [CanBeNull]
@@ -73,8 +71,6 @@ namespace osu.Game.Rulesets.Taiko.UI
 
             this.ScaleTo(3f, 1000, Easing.OutQuint);
             this.FadeOut(500);
-
-            Expire(true);
         }
 
         public void AnimateSecondHit()

From c694deb7d605759a3d7c6b7877d85bf422be4425 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Mar 2021 21:16:50 +0900
Subject: [PATCH 277/434] Revert changes to SettingsSourceAttribute class

---
 .../Configuration/SettingSourceAttribute.cs   | 39 +------------------
 1 file changed, 2 insertions(+), 37 deletions(-)

diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 39d7fba32b..cfce615130 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -5,10 +5,8 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
-using Humanizer;
 using JetBrains.Annotations;
 using osu.Framework.Bindables;
-using osu.Framework.Extensions.TypeExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Localisation;
 using osu.Game.Overlays.Settings;
@@ -63,21 +61,12 @@ namespace osu.Game.Configuration
 
     public static class SettingSourceExtensions
     {
-        public static IReadOnlyList<Drawable> CreateSettingsControls(this object obj, bool includeDisabled = true) =>
-            createSettingsControls(obj, obj.GetOrderedSettingsSourceProperties(), includeDisabled).ToArray();
-
-        public static IReadOnlyList<Drawable> CreateSettingsControlsFromAllBindables(this object obj, bool includeDisabled = true) =>
-            createSettingsControls(obj, obj.GetSettingsSourcePropertiesFromBindables(), includeDisabled).ToArray();
-
-        private static IEnumerable<Drawable> createSettingsControls(object obj, IEnumerable<(SettingSourceAttribute, PropertyInfo)> sourceAttribs, bool includeDisabled = true)
+        public static IEnumerable<Drawable> CreateSettingsControls(this object obj)
         {
-            foreach (var (attr, property) in sourceAttribs)
+            foreach (var (attr, property) in obj.GetOrderedSettingsSourceProperties())
             {
                 object value = property.GetValue(obj);
 
-                if ((value as IBindable)?.Disabled == true)
-                    continue;
-
                 switch (value)
                 {
                     case BindableNumber<float> bNumber:
@@ -150,30 +139,6 @@ namespace osu.Game.Configuration
             }
         }
 
-        public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourcePropertiesFromBindables(this object obj)
-        {
-            HashSet<string> handledProperties = new HashSet<string>();
-
-            // reverse and de-dupe properties to surface base class settings to the top of return order.
-            foreach (var type in obj.GetType().EnumerateBaseTypes().Reverse())
-            {
-                foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
-                {
-                    if (handledProperties.Contains(property.Name))
-                        continue;
-
-                    handledProperties.Add(property.Name);
-
-                    if (typeof(IBindable).IsAssignableFrom(property.PropertyType))
-                    {
-                        var val = property.GetValue(obj);
-                        string description = (val as IHasDescription)?.Description ?? string.Empty;
-                        yield return (new SettingSourceAttribute(property.Name.Humanize(), description), property);
-                    }
-                }
-            }
-        }
-
         public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
         {
             foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))

From a8cc3a3b4468c885504c69ef9fbdb0dc62df059c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 18 Mar 2021 21:17:04 +0900
Subject: [PATCH 278/434] Implement enable state changes locally for
 InputHandlers which should be toggleable

---
 .../Settings/Sections/InputSection.cs         | 21 ++++++++++++-------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index e6aaa1ade9..8d5944f5bf 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -9,7 +9,6 @@ using osu.Framework.Input.Handlers.Joystick;
 using osu.Framework.Input.Handlers.Midi;
 using osu.Framework.Input.Handlers.Mouse;
 using osu.Framework.Platform;
-using osu.Game.Configuration;
 using osu.Game.Overlays.Settings.Sections.Input;
 
 namespace osu.Game.Overlays.Settings.Sections
@@ -70,13 +69,6 @@ namespace osu.Game.Overlays.Settings.Sections
                     return null;
             }
 
-            var settingsControls = handler.CreateSettingsControlsFromAllBindables(false);
-
-            if (settingsControls.Count == 0)
-                return null;
-
-            section.AddRange(settingsControls);
-
             return section;
         }
 
@@ -89,6 +81,19 @@ namespace osu.Game.Overlays.Settings.Sections
                 this.handler = handler;
             }
 
+            [BackgroundDependencyLoader]
+            private void load()
+            {
+                Children = new Drawable[]
+                {
+                    new SettingsCheckbox
+                    {
+                        LabelText = "Enabled",
+                        Current = handler.Enabled
+                    },
+                };
+            }
+
             protected override string Header => handler.Description;
         }
     }

From b419d2c2e27762af7701c1823ae9e7786a730e0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Thu, 18 Mar 2021 19:52:38 +0100
Subject: [PATCH 279/434] Fix invalid xmldoc indent

---
 osu.Game/Skinning/LegacySkin.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 571d65e28b..b69e99773c 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -506,7 +506,7 @@ namespace osu.Game.Skinning
             Samples?.Dispose();
         }
 
-          /// <summary>
+        /// <summary>
         /// A sample wrapper which keeps a reference to the contained skin to avoid finalizer garbage collection of the managing SampleStore.
         /// </summary>
         private class LegacySkinSample : ISample

From b9761c819629f64d647d6178e7bbd7c9987b2b28 Mon Sep 17 00:00:00 2001
From: voidedWarranties <wz68.scratch@gmail.com>
Date: Thu, 18 Mar 2021 16:20:31 -0700
Subject: [PATCH 280/434] Further simplify logic

---
 osu.Game/Screens/Edit/EditorClock.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index e227bd29bf..d0197ce1ec 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -226,15 +226,15 @@ namespace osu.Game.Screens.Edit
         {
             underlyingClock.ProcessFrame();
 
-            var playbackAlreadyStopped = playbackFinished;
             playbackFinished = CurrentTime >= TrackLength;
 
-            if (playbackFinished && !playbackAlreadyStopped)
+            if (playbackFinished)
             {
                 if (IsRunning)
                     underlyingClock.Stop();
 
-                underlyingClock.Seek(TrackLength);
+                if (CurrentTime > TrackLength)
+                    underlyingClock.Seek(TrackLength);
             }
         }
 

From 5f31304d05fa71b356651643608aa34f8207e323 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 14:00:26 +0900
Subject: [PATCH 281/434] Give each type of slider path type a unique colour to
 help visually distinguish them

---
 .../Components/PathControlPointPiece.cs       | 24 ++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index e9838de63d..311ab8ee62 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -12,6 +12,7 @@ using osu.Framework.Input.Events;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Screens.Edit;
 using osuTK;
@@ -195,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
 
             markerRing.Alpha = IsSelected.Value ? 1 : 0;
 
-            Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+            Color4 colour = getColourFromNodeType();
 
             if (IsHovered || IsSelected.Value)
                 colour = colour.Lighten(1);
@@ -203,5 +204,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
             marker.Colour = colour;
             marker.Scale = new Vector2(slider.Scale);
         }
+
+        private Color4 getColourFromNodeType()
+        {
+            if (!(ControlPoint.Type.Value is PathType pathType))
+                return colours.Yellow;
+
+            switch (pathType)
+            {
+                case PathType.Catmull:
+                    return colours.Seafoam;
+
+                case PathType.Bezier:
+                    return colours.Pink;
+
+                case PathType.PerfectCurve:
+                    return colours.PurpleDark;
+
+                default:
+                    return colours.Red;
+            }
+        }
     }
 }

From 0e821e857e34f810e1ede0c9f28df6678602caf3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 15:23:31 +0900
Subject: [PATCH 282/434] Remove unnecessary duplicated skin changed handling

For some reason we were handling this both in `DrawableSkinnableSound`
and `PoolableSkinnableSample` in very similar ways. Only one seems
required.
---
 osu.Game/Skinning/SkinnableSound.cs | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index edd3a2cdd3..e447f9c44c 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -139,12 +139,6 @@ namespace osu.Game.Skinning
             samplesContainer.ForEach(c => c.Stop());
         }
 
-        protected override void SkinChanged(ISkinSource skin, bool allowFallback)
-        {
-            base.SkinChanged(skin, allowFallback);
-            updateSamples();
-        }
-
         private void updateSamples()
         {
             bool wasPlaying = IsPlaying;

From bf4317d3f06ecd3ba29c2f43c33561e5557c3b24 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 15:34:19 +0900
Subject: [PATCH 283/434] Ensure looping is disabled on old samples when
 switching skins

---
 osu.Game/Skinning/PoolableSkinnableSample.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 9103a6a960..09e087d0f2 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -83,6 +83,14 @@ namespace osu.Game.Skinning
 
             bool wasPlaying = Playing;
 
+            if (activeChannel != null)
+            {
+                // when switching away from previous samples, we don't want to call Stop() on them as it sounds better to let them play out.
+                // this may change in the future if we use PoolableSkinSample in more locations than gameplay.
+                // we *do* want to turn off looping, else we end up with an infinite looping sample running in the background.
+                activeChannel.Looping = false;
+            }
+
             sampleContainer.Clear();
             Sample = null;
 

From 9491e6394a65de287bb05c96188db69ca7837158 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 15:46:43 +0900
Subject: [PATCH 284/434] Include the bundled skins when selecting a random
 skin

---
 osu.Game/Skinning/SkinManager.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 601b77e782..752c742a45 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Skinning
         public void SelectRandomSkin()
         {
             // choose from only user skins, removing the current selection to ensure a new one is chosen.
-            var randomChoices = GetAllUsableSkins().Where(s => s.ID > 0 && s.ID != CurrentSkinInfo.Value.ID).ToArray();
+            var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray();
 
             if (randomChoices.Length == 0)
             {

From a9c4fa442a983534897b6d91e3fe9dd60d97dead Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 16:47:39 +0900
Subject: [PATCH 285/434] Avoid potential crash if an overlay is toggled before
 it has been loaded

---
 osu.Game/OsuGame.cs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 66b9141ce8..e5e1f6946e 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -756,6 +756,10 @@ namespace osu.Game
 
         private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
         {
+            // generally shouldn't ever hit this state, but protects against a crash on attempting to change ChildDepth.
+            if (overlay.LoadState < LoadState.Ready)
+                return;
+
             otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
 
             // show above others if not visible at all, else leave at current depth.

From 27c38db14dee71d4f5a40300acbe90eb84d8bf3e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 16:58:08 +0900
Subject: [PATCH 286/434] Add tooltips for slider path nodes which aren't
 inheriting

---
 .../Blueprints/Sliders/Components/PathControlPointPiece.cs   | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index 311ab8ee62..1390675a1a 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
 using osu.Framework.Extensions.Color4Extensions;
 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;
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
     /// <summary>
     /// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>.
     /// </summary>
-    public class PathControlPointPiece : BlueprintPiece<Slider>
+    public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
     {
         public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
 
@@ -225,5 +226,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
                     return colours.Red;
             }
         }
+
+        public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
     }
 }

From 0195d654cacb2e496ce7cc2b3c3b3991111eabb3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 17:09:49 +0900
Subject: [PATCH 287/434] Increase the precision of speed multiplier to match
 osu-stable

---
 osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 0bc5605051..73337ab6f5 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.ControlPoints
         /// </summary>
         public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
         {
-            Precision = 0.1,
+            Precision = 0.01,
             Default = 1,
             MinValue = 0.1,
             MaxValue = 10

From 32c571fc94a0ca6521b25226670f79238dc9bf98 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 17:13:30 +0900
Subject: [PATCH 288/434] Adjust keyboard step to be something sensible

---
 osu.Game/Screens/Edit/Timing/DifficultySection.cs      | 3 ++-
 osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs | 9 +++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
index b87b8961f8..9d80ca0b14 100644
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Timing
             {
                 multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier")
                 {
-                    Current = new DifficultyControlPoint().SpeedMultiplierBindable
+                    Current = new DifficultyControlPoint().SpeedMultiplierBindable,
+                    KeyboardStep = 0.1f
                 }
             });
         }
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
index f2f9f76143..10a5771520 100644
--- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
@@ -69,6 +69,15 @@ namespace osu.Game.Screens.Edit.Timing
             }, true);
         }
 
+        /// <summary>
+        /// A custom step value for each key press which actuates a change on this control.
+        /// </summary>
+        public float KeyboardStep
+        {
+            get => slider.KeyboardStep;
+            set => slider.KeyboardStep = value;
+        }
+
         public Bindable<T> Current
         {
             get => slider.Current;

From 563a0584d589bf0dce7c29fffc03268b097db485 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 18:48:51 +0900
Subject: [PATCH 289/434] Implement editor timeline stacking support

---
 .../Timeline/TimelineBlueprintContainer.cs    | 50 ++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 1fc529910b..4522418e87 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -2,6 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -121,6 +123,46 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
             }
 
             base.Update();
+
+            updateStacking();
+        }
+
+        private void updateStacking()
+        {
+            // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update.
+
+            const int stack_offset = 5;
+
+            // after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints.
+            const int stack_reset_count = 3;
+
+            Stack<double> currentConcurrentObjects = new Stack<double>();
+
+            foreach (var b in SelectionBlueprints.Reverse())
+            {
+                while (currentConcurrentObjects.TryPeek(out double stackEndTime))
+                {
+                    if (Precision.AlmostBigger(stackEndTime, b.HitObject.StartTime, 1))
+                        break;
+
+                    currentConcurrentObjects.Pop();
+                }
+
+                b.Y = -(stack_offset * currentConcurrentObjects.Count);
+
+                var bEndTime = b.HitObject.GetEndTime();
+
+                // if the stack gets too high, we should have space below it to display the next batch of objects.
+                // importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves.
+                if (!currentConcurrentObjects.TryPeek(out double nextStackEndTime) ||
+                    !Precision.AlmostEquals(nextStackEndTime, bEndTime, 1))
+                {
+                    if (currentConcurrentObjects.Count >= stack_reset_count)
+                        currentConcurrentObjects.Clear();
+                }
+
+                currentConcurrentObjects.Push(bEndTime);
+            }
         }
 
         protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
@@ -203,7 +245,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
                 Box.X = Math.Min(rescaledStart, rescaledEnd);
                 Box.Width = Math.Abs(rescaledStart - rescaledEnd);
 
-                PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat);
+                var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
+
+                // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
+                boxScreenRect.Y -= boxScreenRect.Height;
+                boxScreenRect.Height *= 2;
+
+                PerformSelection?.Invoke(boxScreenRect);
             }
 
             public override void Hide()

From 1c865682ae3b0d4116fa8483b1e173c67ab079a3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 16:25:50 +0900
Subject: [PATCH 290/434] Add tablet configuration tests

---
 .../Settings/TestSceneTabletAreaSelection.cs  | 104 ++++++++++++++++++
 1 file changed, 104 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs
new file mode 100644
index 0000000000..30e265baaa
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using OpenTabletDriver.Plugin.Tablet;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+    [TestFixture]
+    public class TestSceneTabletAreaSelection : OsuTestScene
+    {
+        private TabletAreaSelection areaSelection;
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            DigitizerIdentifier testTablet = new DigitizerIdentifier
+            {
+                // size specifications in millimetres.
+                Width = 160,
+                Height = 100,
+            };
+
+            AddRange(new[]
+            {
+                areaSelection = new TabletAreaSelection(testTablet)
+                {
+                    State = { Value = Visibility.Visible }
+                }
+            });
+        }
+    }
+
+    public class TabletAreaSelection : OsuFocusedOverlayContainer
+    {
+        private readonly DigitizerIdentifier tablet;
+
+        private readonly Container tabletContainer;
+        private readonly Container usableAreaContainer;
+
+        public TabletAreaSelection(DigitizerIdentifier tablet)
+        {
+            RelativeSizeAxes = Axes.Both;
+
+            this.tablet = tablet;
+
+            InternalChildren = new Drawable[]
+            {
+                tabletContainer = new Container
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Scale = new Vector2(3),
+                    Children = new Drawable[]
+                    {
+                        new Box
+                        {
+                            RelativeSizeAxes = Axes.Both,
+                            Colour = Color4.White,
+                        },
+                        usableAreaContainer = new Container
+                        {
+                            Children = new Drawable[]
+                            {
+                                new Box
+                                {
+                                    RelativeSizeAxes = Axes.Both,
+                                    Colour = Color4.Yellow,
+                                },
+                                new OsuSpriteText
+                                {
+                                    Text = "usable area",
+                                    Anchor = Anchor.Centre,
+                                    Origin = Anchor.Centre,
+                                    Colour = Color4.Black,
+                                    Font = OsuFont.Default.With(size: 12)
+                                }
+                            }
+                        },
+                    }
+                }
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            // TODO: handle tablet device changes etc.
+            tabletContainer.Size = new Vector2(tablet.Width, tablet.Height);
+
+            usableAreaContainer.Position = new Vector2(10, 30);
+            usableAreaContainer.Size = new Vector2(80, 60);
+        }
+    }
+}

From d026c8da851a5fbd399206b8efff28b76c09c643 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 15 Mar 2021 18:37:46 +0900
Subject: [PATCH 291/434] Initial pass of configuration interface

---
 .../Settings/TestSceneTabletSettings.cs       |  40 +++++++
 .../Sections/Input/TabletAreaSelection.cs     |  79 ++++++-------
 .../Settings/Sections/Input/TabletSettings.cs | 111 ++++++++++++++++++
 .../Settings/Sections/InputSection.cs         |   6 +
 4 files changed, 194 insertions(+), 42 deletions(-)
 create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
 rename osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs => osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs (55%)
 create mode 100644 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
new file mode 100644
index 0000000000..be5b355e06
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Drawing;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Platform;
+using osu.Game.Overlays.Settings.Sections.Input;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+    [TestFixture]
+    public class TestSceneTabletSettings : OsuTestScene
+    {
+        [BackgroundDependencyLoader]
+        private void load(GameHost host)
+        {
+            var tabletHandler = host.AvailableInputHandlers.OfType<ITabletHandler>().FirstOrDefault();
+
+            if (tabletHandler == null)
+                return;
+
+            tabletHandler.AreaOffset.MinValue = new Size(0, 0);
+            tabletHandler.AreaOffset.MaxValue = new Size(160, 100);
+            tabletHandler.AreaOffset.Value = new Size(10, 10);
+
+            tabletHandler.AreaSize.MinValue = new Size(0, 0);
+            tabletHandler.AreaSize.MaxValue = new Size(160, 100);
+            tabletHandler.AreaSize.Value = new Size(100, 80);
+
+            AddRange(new Drawable[]
+            {
+                new TabletSettings(tabletHandler),
+            });
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
similarity index 55%
rename from osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs
rename to osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 30e265baaa..31a2768735 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -1,65 +1,46 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using NUnit.Framework;
-using OpenTabletDriver.Plugin.Tablet;
+using System.Drawing;
 using osu.Framework.Allocation;
+using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Handlers.Tablet;
 using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osuTK;
+
 using osuTK.Graphics;
 
-namespace osu.Game.Tests.Visual.Settings
+namespace osu.Game.Overlays.Settings.Sections.Input
 {
-    [TestFixture]
-    public class TestSceneTabletAreaSelection : OsuTestScene
+    public class TabletAreaSelection : CompositeDrawable
     {
-        private TabletAreaSelection areaSelection;
-
-        [BackgroundDependencyLoader]
-        private void load()
-        {
-            DigitizerIdentifier testTablet = new DigitizerIdentifier
-            {
-                // size specifications in millimetres.
-                Width = 160,
-                Height = 100,
-            };
-
-            AddRange(new[]
-            {
-                areaSelection = new TabletAreaSelection(testTablet)
-                {
-                    State = { Value = Visibility.Visible }
-                }
-            });
-        }
-    }
-
-    public class TabletAreaSelection : OsuFocusedOverlayContainer
-    {
-        private readonly DigitizerIdentifier tablet;
+        private readonly ITabletHandler handler;
 
         private readonly Container tabletContainer;
         private readonly Container usableAreaContainer;
 
-        public TabletAreaSelection(DigitizerIdentifier tablet)
-        {
-            RelativeSizeAxes = Axes.Both;
+        private readonly Bindable<Size> areaOffset = new BindableSize();
+        private readonly Bindable<Size> areaSize = new BindableSize();
+        private readonly Bindable<Size> tabletSize = new BindableSize();
 
-            this.tablet = tablet;
+        public TabletAreaSelection(ITabletHandler handler)
+        {
+            this.handler = handler;
+
+            Padding = new MarginPadding(5);
 
             InternalChildren = new Drawable[]
             {
                 tabletContainer = new Container
                 {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    Scale = new Vector2(3),
+                    Masking = true,
+                    CornerRadius = 5,
+                    BorderThickness = 2,
+                    BorderColour = Color4.Black,
                     Children = new Drawable[]
                     {
                         new Box
@@ -69,6 +50,8 @@ namespace osu.Game.Tests.Visual.Settings
                         },
                         usableAreaContainer = new Container
                         {
+                            Masking = true,
+                            CornerRadius = 5,
                             Children = new Drawable[]
                             {
                                 new Box
@@ -94,11 +77,23 @@ namespace osu.Game.Tests.Visual.Settings
         [BackgroundDependencyLoader]
         private void load()
         {
-            // TODO: handle tablet device changes etc.
-            tabletContainer.Size = new Vector2(tablet.Width, tablet.Height);
+            areaOffset.BindTo(handler.AreaOffset);
+            areaOffset.BindValueChanged(val =>
+            {
+                usableAreaContainer.MoveTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
+            }, true);
 
-            usableAreaContainer.Position = new Vector2(10, 30);
-            usableAreaContainer.Size = new Vector2(80, 60);
+            areaSize.BindTo(handler.AreaSize);
+            areaSize.BindValueChanged(val =>
+            {
+                usableAreaContainer.ResizeTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
+            }, true);
+
+            ((IBindable<Size>)tabletSize).BindTo(handler.TabletSize);
+            tabletSize.BindValueChanged(val =>
+            {
+                tabletContainer.ResizeTo(new Vector2(tabletSize.Value.Width, tabletSize.Value.Height), 100, Easing.OutQuint);
+            }, true);
         }
     }
 }
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
new file mode 100644
index 0000000000..5df9c879eb
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -0,0 +1,111 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Drawing;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Game.Configuration;
+
+namespace osu.Game.Overlays.Settings.Sections.Input
+{
+    public class TabletSettings : SettingsSubsection
+    {
+        private readonly ITabletHandler tabletHandler;
+
+        private readonly BindableSize areaOffset = new BindableSize();
+        private readonly BindableSize areaSize = new BindableSize();
+        private readonly BindableSize tabletSize = new BindableSize();
+
+        private readonly BindableNumber<int> offsetX = new BindableNumber<int> { MinValue = 0 };
+        private readonly BindableNumber<int> offsetY = new BindableNumber<int> { MinValue = 0 };
+
+        private readonly BindableNumber<int> sizeX = new BindableNumber<int> { MinValue = 0 };
+        private readonly BindableNumber<int> sizeY = new BindableNumber<int> { MinValue = 0 };
+
+        protected override string Header => "Tablet";
+
+        public TabletSettings(ITabletHandler tabletHandler)
+        {
+            this.tabletHandler = tabletHandler;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
+        {
+            // TODO: this should all eventually be replaced with a control that handles BindableSize.
+            areaOffset.BindTo(tabletHandler.AreaOffset);
+            areaOffset.BindValueChanged(val =>
+            {
+                offsetX.Value = val.NewValue.Width;
+                offsetY.Value = val.NewValue.Height;
+            }, true);
+
+            offsetX.BindValueChanged(val => areaOffset.Value = new Size(val.NewValue, areaOffset.Value.Height));
+            offsetY.BindValueChanged(val => areaOffset.Value = new Size(areaOffset.Value.Width, val.NewValue));
+
+            areaSize.BindTo(tabletHandler.AreaSize);
+            areaSize.BindValueChanged(val =>
+            {
+                sizeX.Value = val.NewValue.Width;
+                sizeY.Value = val.NewValue.Height;
+            }, true);
+
+            sizeX.BindValueChanged(val => areaSize.Value = new Size(val.NewValue, areaSize.Value.Height));
+            sizeY.BindValueChanged(val => areaSize.Value = new Size(areaSize.Value.Width, val.NewValue));
+
+            ((IBindable<Size>)tabletSize).BindTo(tabletHandler.TabletSize);
+            tabletSize.BindValueChanged(val =>
+            {
+                // todo: these should propagate from a TabletChanged event or similar.
+                offsetX.MaxValue = val.NewValue.Width;
+                sizeX.Default = sizeX.MaxValue = val.NewValue.Width;
+
+                offsetY.MaxValue = val.NewValue.Height;
+                sizeY.Default = sizeY.MaxValue = val.NewValue.Height;
+
+                updateDisplay();
+            }, true);
+        }
+
+        private void updateDisplay()
+        {
+            if (tabletSize.Value == System.Drawing.Size.Empty)
+            {
+                Clear();
+                return;
+            }
+
+            Children = new Drawable[]
+            {
+                new SettingsSlider<int>
+                {
+                    LabelText = "Offset X",
+                    Current = offsetX
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Offset Y",
+                    Current = offsetY
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Size X",
+                    Current = sizeX
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Size Y",
+                    Current = sizeY
+                },
+                new TabletAreaSelection(tabletHandler)
+                {
+                    RelativeSizeAxes = Axes.X,
+                    Height = 100,
+                }
+            };
+        }
+    }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 8d5944f5bf..6e99891794 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -8,6 +8,7 @@ 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.Platform;
 using osu.Game.Overlays.Settings.Sections.Input;
 
@@ -55,6 +56,11 @@ namespace osu.Game.Overlays.Settings.Sections
 
             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;

From 3b7edf13337a26a9d358b2dd70eef5773ba73332 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 14:45:21 +0900
Subject: [PATCH 292/434] Make tablet display always fit to size of settings
 area

---
 .../Settings/TestSceneTabletSettings.cs       |  26 ++++-
 .../Sections/Input/TabletAreaSelection.cs     | 105 ++++++++++--------
 2 files changed, 78 insertions(+), 53 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index be5b355e06..6455f51ab9 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -2,9 +2,9 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Drawing;
-using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Handlers.Tablet;
 using osu.Framework.Platform;
@@ -18,16 +18,13 @@ namespace osu.Game.Tests.Visual.Settings
         [BackgroundDependencyLoader]
         private void load(GameHost host)
         {
-            var tabletHandler = host.AvailableInputHandlers.OfType<ITabletHandler>().FirstOrDefault();
-
-            if (tabletHandler == null)
-                return;
+            var tabletHandler = new TestTabletHandler();
 
             tabletHandler.AreaOffset.MinValue = new Size(0, 0);
             tabletHandler.AreaOffset.MaxValue = new Size(160, 100);
             tabletHandler.AreaOffset.Value = new Size(10, 10);
 
-            tabletHandler.AreaSize.MinValue = new Size(0, 0);
+            tabletHandler.AreaSize.MinValue = new Size(10, 10);
             tabletHandler.AreaSize.MaxValue = new Size(160, 100);
             tabletHandler.AreaSize.Value = new Size(100, 80);
 
@@ -35,6 +32,23 @@ namespace osu.Game.Tests.Visual.Settings
             {
                 new TabletSettings(tabletHandler),
             });
+
+            AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Size(160, 100)));
+            AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Size(300, 300)));
+            AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 300)));
+            AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 700)));
+        }
+
+        public class TestTabletHandler : ITabletHandler
+        {
+            private readonly Bindable<Size> tabletSize = new Bindable<Size>();
+
+            public BindableSize AreaOffset { get; } = new BindableSize();
+            public BindableSize AreaSize { get; } = new BindableSize();
+            public IBindable<Size> TabletSize => tabletSize;
+            public BindableBool Enabled { get; } = new BindableBool(true);
+
+            public void SetTabletSize(Size size) => tabletSize.Value = size;
         }
     }
 }
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 31a2768735..775aceb5f9 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -11,7 +12,6 @@ using osu.Framework.Input.Handlers.Tablet;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osuTK;
-
 using osuTK.Graphics;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
@@ -20,8 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
     {
         private readonly ITabletHandler handler;
 
-        private readonly Container tabletContainer;
-        private readonly Container usableAreaContainer;
+        private Container tabletContainer;
+        private Container usableAreaContainer;
 
         private readonly Bindable<Size> areaOffset = new BindableSize();
         private readonly Bindable<Size> areaSize = new BindableSize();
@@ -30,53 +30,50 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         public TabletAreaSelection(ITabletHandler handler)
         {
             this.handler = handler;
-
-            Padding = new MarginPadding(5);
-
-            InternalChildren = new Drawable[]
-            {
-                tabletContainer = new Container
-                {
-                    Masking = true,
-                    CornerRadius = 5,
-                    BorderThickness = 2,
-                    BorderColour = Color4.Black,
-                    Children = new Drawable[]
-                    {
-                        new Box
-                        {
-                            RelativeSizeAxes = Axes.Both,
-                            Colour = Color4.White,
-                        },
-                        usableAreaContainer = new Container
-                        {
-                            Masking = true,
-                            CornerRadius = 5,
-                            Children = new Drawable[]
-                            {
-                                new Box
-                                {
-                                    RelativeSizeAxes = Axes.Both,
-                                    Colour = Color4.Yellow,
-                                },
-                                new OsuSpriteText
-                                {
-                                    Text = "usable area",
-                                    Anchor = Anchor.Centre,
-                                    Origin = Anchor.Centre,
-                                    Colour = Color4.Black,
-                                    Font = OsuFont.Default.With(size: 12)
-                                }
-                            }
-                        },
-                    }
-                }
-            };
         }
 
         [BackgroundDependencyLoader]
         private void load()
         {
+            Padding = new MarginPadding(5);
+
+            InternalChild = tabletContainer = new Container
+            {
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                Masking = true,
+                CornerRadius = 5,
+                BorderThickness = 2,
+                BorderColour = Color4.Black,
+                Children = new Drawable[]
+                {
+                    new Box
+                    {
+                        RelativeSizeAxes = Axes.Both,
+                        Colour = Color4.White,
+                    },
+                    usableAreaContainer = new Container
+                    {
+                        Children = new Drawable[]
+                        {
+                            new Box
+                            {
+                                RelativeSizeAxes = Axes.Both,
+                                Colour = Color4.Yellow,
+                            },
+                            new OsuSpriteText
+                            {
+                                Text = "usable area",
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                                Colour = Color4.Black,
+                                Font = OsuFont.Default.With(size: 12)
+                            }
+                        }
+                    },
+                }
+            };
+
             areaOffset.BindTo(handler.AreaOffset);
             areaOffset.BindValueChanged(val =>
             {
@@ -92,8 +89,22 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             ((IBindable<Size>)tabletSize).BindTo(handler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
-                tabletContainer.ResizeTo(new Vector2(tabletSize.Value.Width, tabletSize.Value.Height), 100, Easing.OutQuint);
-            }, true);
+                tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
+            });
+        }
+
+        protected override void Update()
+        {
+            base.Update();
+
+            var size = tabletSize.Value;
+
+            float fitX = size.Width / DrawWidth;
+            float fitY = size.Height / DrawHeight;
+
+            float adjust = MathF.Max(fitX, fitY);
+
+            tabletContainer.Scale = new Vector2(1 / adjust);
         }
     }
 }

From 926e40925ef0ba15bb6a3994f71e291dfd7196c5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 15:57:06 +0900
Subject: [PATCH 293/434] Add exclude rule to fix dynamic compilations issues
 with settings sections

---
 osu.Game/Overlays/Settings/SettingsSubsection.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs
index 1b82d973e9..6abf6283b9 100644
--- a/osu.Game/Overlays/Settings/SettingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs
@@ -8,10 +8,12 @@ using osu.Game.Graphics.Sprites;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
+using osu.Framework.Testing;
 using osu.Game.Graphics;
 
 namespace osu.Game.Overlays.Settings
 {
+    [ExcludeFromDynamicCompile]
     public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren
     {
         protected override Container<Drawable> Content => FlowContent;

From 0a6525baee8afa45b44432e8acc0e13fb0ca1be7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 15:57:29 +0900
Subject: [PATCH 294/434] Fix slider bars reloading each time the tablet size
 is changed

---
 .../Settings/Sections/Input/TabletSettings.cs     | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 5df9c879eb..ac4a42e984 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -4,10 +4,8 @@
 using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Handlers.Tablet;
-using osu.Game.Configuration;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
 {
@@ -33,9 +31,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         }
 
         [BackgroundDependencyLoader]
-        private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
+        private void load()
         {
-            // TODO: this should all eventually be replaced with a control that handles BindableSize.
             areaOffset.BindTo(tabletHandler.AreaOffset);
             areaOffset.BindValueChanged(val =>
             {
@@ -59,6 +56,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             ((IBindable<Size>)tabletSize).BindTo(tabletHandler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
+                if (tabletSize.Value == System.Drawing.Size.Empty)
+                    return;
+
                 // todo: these should propagate from a TabletChanged event or similar.
                 offsetX.MaxValue = val.NewValue.Width;
                 sizeX.Default = sizeX.MaxValue = val.NewValue.Width;
@@ -72,11 +72,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private void updateDisplay()
         {
-            if (tabletSize.Value == System.Drawing.Size.Empty)
-            {
-                Clear();
+            if (Children.Count > 0)
                 return;
-            }
 
             Children = new Drawable[]
             {
@@ -103,7 +100,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 new TabletAreaSelection(tabletHandler)
                 {
                     RelativeSizeAxes = Axes.X,
-                    Height = 100,
+                    Height = 300,
                 }
             };
         }

From 94f184d113200bdca2c7c89496948a9e2e3d9990 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 16:02:39 +0900
Subject: [PATCH 295/434] Add feedback when area extends beyond tablet size

---
 .../Sections/Input/TabletAreaSelection.cs      | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 775aceb5f9..77b16a970d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                             new Box
                             {
                                 RelativeSizeAxes = Axes.Both,
-                                Colour = Color4.Yellow,
+                                Colour = Color4.White,
                             },
                             new OsuSpriteText
                             {
@@ -78,21 +78,37 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             areaOffset.BindValueChanged(val =>
             {
                 usableAreaContainer.MoveTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
+                checkBounds();
             }, true);
 
             areaSize.BindTo(handler.AreaSize);
             areaSize.BindValueChanged(val =>
             {
                 usableAreaContainer.ResizeTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
+                checkBounds();
             }, true);
 
             ((IBindable<Size>)tabletSize).BindTo(handler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
                 tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
+                checkBounds();
             });
         }
 
+        [Resolved]
+        private OsuColour colour { get; set; }
+
+        private void checkBounds()
+        {
+            Size areaExtent = areaOffset.Value + areaSize.Value;
+
+            bool isWithinBounds = areaExtent.Width < tabletSize.Value.Width
+                                  && areaExtent.Height < tabletSize.Value.Height;
+
+            usableAreaContainer.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
+        }
+
         protected override void Update()
         {
             base.Update();

From 464702182d6064023064720824403e8277bd19a1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 16:23:46 +0900
Subject: [PATCH 296/434] Consume device name

---
 .../Visual/Settings/TestSceneTabletSettings.cs     |  1 +
 .../Settings/Sections/Input/TabletAreaSelection.cs | 14 +++++++++++---
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 6455f51ab9..1c9cd6c2ba 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -46,6 +46,7 @@ namespace osu.Game.Tests.Visual.Settings
             public BindableSize AreaOffset { get; } = new BindableSize();
             public BindableSize AreaSize { get; } = new BindableSize();
             public IBindable<Size> TabletSize => tabletSize;
+            public string DeviceName => "test tablet T-421";
             public BindableBool Enabled { get; } = new BindableBool(true);
 
             public void SetTabletSize(Size size) => tabletSize.Value = size;
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 77b16a970d..54a14cd822 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         private readonly Bindable<Size> areaSize = new BindableSize();
         private readonly Bindable<Size> tabletSize = new BindableSize();
 
+        private OsuSpriteText tabletName;
+
         public TabletAreaSelection(ITabletHandler handler)
         {
             this.handler = handler;
@@ -44,13 +46,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 Masking = true,
                 CornerRadius = 5,
                 BorderThickness = 2,
-                BorderColour = Color4.Black,
+                BorderColour = colour.Gray3,
                 Children = new Drawable[]
                 {
                     new Box
                     {
                         RelativeSizeAxes = Axes.Both,
-                        Colour = Color4.White,
+                        Colour = colour.Gray1,
                     },
                     usableAreaContainer = new Container
                     {
@@ -59,7 +61,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                             new Box
                             {
                                 RelativeSizeAxes = Axes.Both,
-                                Colour = Color4.White,
+                                Alpha = 0.6f,
                             },
                             new OsuSpriteText
                             {
@@ -71,6 +73,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                             }
                         }
                     },
+                    tabletName = new OsuSpriteText
+                    {
+                        Padding = new MarginPadding(3),
+                        Font = OsuFont.Default.With(size: 8)
+                    },
                 }
             };
 
@@ -92,6 +99,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             tabletSize.BindValueChanged(val =>
             {
                 tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
+                tabletName.Text = handler.DeviceName;
                 checkBounds();
             });
         }

From 382109c7a221f83b02b00a70e002551e7244157f Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 16:24:08 +0900
Subject: [PATCH 297/434] Make test scene feel more like settings (width-wise)

---
 .../Visual/Settings/TestSceneTabletSettings.cs           | 9 ++++++++-
 osu.Game/Overlays/SettingsPanel.cs                       | 2 +-
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 1c9cd6c2ba..3d65db9420 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Handlers.Tablet;
 using osu.Framework.Platform;
+using osu.Game.Overlays;
 using osu.Game.Overlays.Settings.Sections.Input;
 
 namespace osu.Game.Tests.Visual.Settings
@@ -30,7 +31,13 @@ namespace osu.Game.Tests.Visual.Settings
 
             AddRange(new Drawable[]
             {
-                new TabletSettings(tabletHandler),
+                new TabletSettings(tabletHandler)
+                {
+                    RelativeSizeAxes = Axes.None,
+                    Width = SettingsPanel.WIDTH,
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
+                }
             });
 
             AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Size(160, 100)));
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index f1270f750e..8f3274b2b5 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Overlays
 
         private const float sidebar_width = Sidebar.DEFAULT_WIDTH;
 
-        protected const float WIDTH = 400;
+        public const float WIDTH = 400;
 
         protected Container<Drawable> ContentContainer;
 

From 2dc2cb04c317de94a762dbdb74dfede0a91b33ee Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 16:24:20 +0900
Subject: [PATCH 298/434] Fix bounds check becoming false when using full area

---
 .../Overlays/Settings/Sections/Input/TabletAreaSelection.cs   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 54a14cd822..6a3cc46e2b 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -111,8 +111,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             Size areaExtent = areaOffset.Value + areaSize.Value;
 
-            bool isWithinBounds = areaExtent.Width < tabletSize.Value.Width
-                                  && areaExtent.Height < tabletSize.Value.Height;
+            bool isWithinBounds = areaExtent.Width <= tabletSize.Value.Width
+                                  && areaExtent.Height <= tabletSize.Value.Height;
 
             usableAreaContainer.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
         }

From 9b70f0ee1fca97f9d8d686c131dd66f5a8be20ef Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 16:24:51 +0900
Subject: [PATCH 299/434] Tidy up visual appearance of settings and add a reset
 button

---
 .../Settings/Sections/Input/TabletSettings.cs | 57 ++++++++++++-------
 1 file changed, 36 insertions(+), 21 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index ac4a42e984..ca0a0349ab 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 offsetY.MaxValue = val.NewValue.Height;
                 sizeY.Default = sizeY.MaxValue = val.NewValue.Height;
 
+                areaSize.Default = new Size(sizeX.Default, sizeY.Default);
+
                 updateDisplay();
             }, true);
         }
@@ -77,31 +79,44 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             Children = new Drawable[]
             {
-                new SettingsSlider<int>
-                {
-                    LabelText = "Offset X",
-                    Current = offsetX
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Offset Y",
-                    Current = offsetY
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Size X",
-                    Current = sizeX
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Size Y",
-                    Current = sizeY
-                },
                 new TabletAreaSelection(tabletHandler)
                 {
                     RelativeSizeAxes = Axes.X,
                     Height = 300,
-                }
+                },
+                new SettingsButton
+                {
+                    Text = "Reset to full area",
+                    Action = () =>
+                    {
+                        areaOffset.SetDefault();
+                        areaSize.SetDefault();
+                    },
+                },
+                new SettingsCheckbox
+                {
+                    LabelText = "Lock aspect ratio",
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "X Offset",
+                    Current = offsetX
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Y Offset",
+                    Current = offsetY
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Width",
+                    Current = sizeX
+                },
+                new SettingsSlider<int>
+                {
+                    LabelText = "Height",
+                    Current = sizeY
+                },
             };
         }
     }

From 43359553c1a3f6326ce13c0080440667ff13548e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 17:04:22 +0900
Subject: [PATCH 300/434] Add aspect ratio display and limiting

---
 .../Settings/Sections/Input/TabletSettings.cs | 46 +++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index ca0a0349ab..5d85ecf138 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -23,6 +24,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         private readonly BindableNumber<int> sizeX = new BindableNumber<int> { MinValue = 0 };
         private readonly BindableNumber<int> sizeY = new BindableNumber<int> { MinValue = 0 };
 
+        private SettingsButton aspectResetButton;
+
+        private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
+        {
+            MinValue = 0.5f,
+            MaxValue = 2,
+            Precision = 0.01f,
+        };
+
         protected override string Header => "Tablet";
 
         public TabletSettings(ITabletHandler tabletHandler)
@@ -48,6 +58,33 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             {
                 sizeX.Value = val.NewValue.Width;
                 sizeY.Value = val.NewValue.Height;
+
+                float proposedAspectRatio = (float)sizeX.Value / sizeY.Value;
+
+                aspectRatio.Value = proposedAspectRatio;
+
+                if (proposedAspectRatio < aspectRatio.MinValue || proposedAspectRatio > aspectRatio.MaxValue)
+                {
+                    // apply aspect ratio restrictions to keep things in a usable state.
+
+                    // correction is always going to be below 1.
+                    float correction = proposedAspectRatio > aspectRatio.Value
+                        ? aspectRatio.Value / proposedAspectRatio
+                        : proposedAspectRatio / aspectRatio.Value;
+
+                    if (val.NewValue.Width != val.OldValue.Width)
+                    {
+                        if (val.NewValue.Width > val.OldValue.Width)
+                            correction = 1 / correction;
+                        areaSize.Value = new Size(areaSize.Value.Width, (int)(val.NewValue.Height * correction));
+                    }
+                    else
+                    {
+                        if (val.NewValue.Height > val.OldValue.Height)
+                            correction = 1 / correction;
+                        areaSize.Value = new Size((int)(val.NewValue.Width * correction), areaSize.Value.Height);
+                    }
+                }
             }, true);
 
             sizeX.BindValueChanged(val => areaSize.Value = new Size(val.NewValue, areaSize.Value.Height));
@@ -97,6 +134,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 {
                     LabelText = "Lock aspect ratio",
                 },
+                aspectResetButton = new SettingsButton
+                {
+                    Text = "Take aspect ratio from screen size",
+                },
+                new SettingsSlider<float>
+                {
+                    LabelText = "Aspect Ratio",
+                    Current = aspectRatio
+                },
                 new SettingsSlider<int>
                 {
                     LabelText = "X Offset",

From e3bed4c97dff26b9af05609fa100c2d748956b3c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 17:57:50 +0900
Subject: [PATCH 301/434] Simplify aspect ratio application, add window
 conforming and direct adjustment

---
 .../Settings/Sections/Input/TabletSettings.cs | 120 ++++++++++++------
 1 file changed, 81 insertions(+), 39 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 5d85ecf138..9fa74eda18 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -1,12 +1,14 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
+using System.ComponentModel;
 using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Platform;
+using osu.Framework.Threading;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
 {
@@ -21,18 +23,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         private readonly BindableNumber<int> offsetX = new BindableNumber<int> { MinValue = 0 };
         private readonly BindableNumber<int> offsetY = new BindableNumber<int> { MinValue = 0 };
 
-        private readonly BindableNumber<int> sizeX = new BindableNumber<int> { MinValue = 0 };
-        private readonly BindableNumber<int> sizeY = new BindableNumber<int> { MinValue = 0 };
+        private readonly BindableNumber<int> sizeX = new BindableNumber<int> { MinValue = 10 };
+        private readonly BindableNumber<int> sizeY = new BindableNumber<int> { MinValue = 10 };
 
-        private SettingsButton aspectResetButton;
+        [Resolved]
+        private GameHost host { get; set; }
+
+        /// <summary>
+        /// Based on the longest available smartphone.
+        /// </summary>
+        private const float largest_feasible_aspect_ratio = 20f / 9;
 
         private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
         {
-            MinValue = 0.5f,
-            MaxValue = 2,
+            MinValue = 1 / largest_feasible_aspect_ratio,
+            MaxValue = largest_feasible_aspect_ratio,
             Precision = 0.01f,
         };
 
+        private readonly BindableBool aspectLock = new BindableBool();
+
+        private ScheduledDelegate aspectRatioApplication;
+
         protected override string Header => "Tablet";
 
         public TabletSettings(ITabletHandler tabletHandler)
@@ -59,37 +71,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 sizeX.Value = val.NewValue.Width;
                 sizeY.Value = val.NewValue.Height;
 
-                float proposedAspectRatio = (float)sizeX.Value / sizeY.Value;
-
-                aspectRatio.Value = proposedAspectRatio;
-
-                if (proposedAspectRatio < aspectRatio.MinValue || proposedAspectRatio > aspectRatio.MaxValue)
-                {
-                    // apply aspect ratio restrictions to keep things in a usable state.
-
-                    // correction is always going to be below 1.
-                    float correction = proposedAspectRatio > aspectRatio.Value
-                        ? aspectRatio.Value / proposedAspectRatio
-                        : proposedAspectRatio / aspectRatio.Value;
-
-                    if (val.NewValue.Width != val.OldValue.Width)
-                    {
-                        if (val.NewValue.Width > val.OldValue.Width)
-                            correction = 1 / correction;
-                        areaSize.Value = new Size(areaSize.Value.Width, (int)(val.NewValue.Height * correction));
-                    }
-                    else
-                    {
-                        if (val.NewValue.Height > val.OldValue.Height)
-                            correction = 1 / correction;
-                        areaSize.Value = new Size((int)(val.NewValue.Width * correction), areaSize.Value.Height);
-                    }
-                }
+                aspectRatioApplication?.Cancel();
+                aspectRatioApplication = Schedule(() => applyAspectRatio(val));
             }, true);
 
             sizeX.BindValueChanged(val => areaSize.Value = new Size(val.NewValue, areaSize.Value.Height));
             sizeY.BindValueChanged(val => areaSize.Value = new Size(areaSize.Value.Width, val.NewValue));
 
+            aspectRatio.BindValueChanged(aspect =>
+            {
+                forceAspectRatio(aspect.NewValue);
+            });
+
             ((IBindable<Size>)tabletSize).BindTo(tabletHandler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
@@ -109,6 +102,33 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             }, true);
         }
 
+        private void applyAspectRatio(ValueChangedEvent<Size> sizeChanged)
+        {
+            float proposedAspectRatio = (float)sizeX.Value / sizeY.Value;
+
+            if (!aspectLock.Value)
+            {
+                aspectRatio.Value = proposedAspectRatio;
+
+                // aspect ratio was in a valid range.
+                if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
+                    return;
+            }
+
+            if (sizeChanged.NewValue.Width != sizeChanged.OldValue.Width)
+            {
+                areaSize.Value = new Size(areaSize.Value.Width, (int)(areaSize.Value.Width / aspectRatio.Value));
+            }
+            else
+            {
+                areaSize.Value = new Size((int)(areaSize.Value.Height * aspectRatio.Value), areaSize.Value.Height);
+            }
+
+            // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
+            // this avoids a potential feedback loop.
+            aspectRatioApplication?.Cancel();
+        }
+
         private void updateDisplay()
         {
             if (Children.Count > 0)
@@ -121,22 +141,24 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     RelativeSizeAxes = Axes.X,
                     Height = 300,
                 },
-                new SettingsButton
+                new DangerousSettingsButton
                 {
                     Text = "Reset to full area",
                     Action = () =>
                     {
+                        aspectLock.Value = false;
+
                         areaOffset.SetDefault();
                         areaSize.SetDefault();
                     },
                 },
-                new SettingsCheckbox
+                new SettingsButton
                 {
-                    LabelText = "Lock aspect ratio",
-                },
-                aspectResetButton = new SettingsButton
-                {
-                    Text = "Take aspect ratio from screen size",
+                    Text = "Conform to current game aspect ratio",
+                    Action = () =>
+                    {
+                        forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
+                    }
                 },
                 new SettingsSlider<float>
                 {
@@ -153,6 +175,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     LabelText = "Y Offset",
                     Current = offsetY
                 },
+                new SettingsCheckbox
+                {
+                    LabelText = "Lock aspect ratio",
+                    Current = aspectLock
+                },
                 new SettingsSlider<int>
                 {
                     LabelText = "Width",
@@ -165,5 +192,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 },
             };
         }
+
+        private void forceAspectRatio(float aspectRatio)
+        {
+            aspectLock.Value = false;
+
+            int proposedHeight = (int)(sizeX.Value / aspectRatio);
+
+            if (proposedHeight < sizeY.MaxValue)
+                sizeY.Value = proposedHeight;
+            else
+                sizeX.Value = (int)(sizeY.Value * aspectRatio);
+
+            aspectRatioApplication?.Cancel();
+            aspectLock.Value = true;
+        }
     }
 }

From 932745e5c4049e8cf15074f5ecca437364d18fd9 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 18:14:29 +0900
Subject: [PATCH 302/434] Fix remaining feedback loops

---
 .../Settings/Sections/Input/TabletSettings.cs | 51 ++++++++++++-------
 1 file changed, 33 insertions(+), 18 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 9fa74eda18..e94df7dc1b 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.ComponentModel;
 using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -80,7 +79,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             aspectRatio.BindValueChanged(aspect =>
             {
-                forceAspectRatio(aspect.NewValue);
+                aspectRatioApplication?.Cancel();
+                aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
             });
 
             ((IBindable<Size>)tabletSize).BindTo(tabletHandler.TabletSize);
@@ -102,31 +102,44 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             }, true);
         }
 
+        private float curentAspectRatio => (float)sizeX.Value / sizeY.Value;
+
         private void applyAspectRatio(ValueChangedEvent<Size> sizeChanged)
         {
-            float proposedAspectRatio = (float)sizeX.Value / sizeY.Value;
+            float proposedAspectRatio = curentAspectRatio;
 
-            if (!aspectLock.Value)
+            try
             {
-                aspectRatio.Value = proposedAspectRatio;
+                if (!aspectLock.Value)
+                {
+                    // aspect ratio was in a valid range.
+                    if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
+                    {
+                        updateAspectRatio();
+                        return;
+                    }
+                }
 
-                // aspect ratio was in a valid range.
-                if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
-                    return;
+                if (sizeChanged.NewValue.Width != sizeChanged.OldValue.Width)
+                {
+                    areaSize.Value = new Size(areaSize.Value.Width, (int)(areaSize.Value.Width / aspectRatio.Value));
+                }
+                else
+                {
+                    areaSize.Value = new Size((int)(areaSize.Value.Height * aspectRatio.Value), areaSize.Value.Height);
+                }
             }
-
-            if (sizeChanged.NewValue.Width != sizeChanged.OldValue.Width)
+            finally
             {
-                areaSize.Value = new Size(areaSize.Value.Width, (int)(areaSize.Value.Width / aspectRatio.Value));
-            }
-            else
-            {
-                areaSize.Value = new Size((int)(areaSize.Value.Height * aspectRatio.Value), areaSize.Value.Height);
+                // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
+                // this avoids a potential feedback loop.
+                aspectRatioApplication?.Cancel();
             }
+        }
 
-            // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
-            // this avoids a potential feedback loop.
-            aspectRatioApplication?.Cancel();
+        private void updateAspectRatio()
+        {
+            aspectRatio.Value = curentAspectRatio;
         }
 
         private void updateDisplay()
@@ -204,6 +217,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             else
                 sizeX.Value = (int)(sizeY.Value * aspectRatio);
 
+            updateAspectRatio();
+
             aspectRatioApplication?.Cancel();
             aspectLock.Value = true;
         }

From bba25a0182660443283b91c01356742cae676e56 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 18:40:21 +0900
Subject: [PATCH 303/434] Tidy up draw hierarchy and bindable logic

---
 .../Sections/Input/TabletAreaSelection.cs     |   7 +-
 .../Settings/Sections/Input/TabletSettings.cs | 184 +++++++++---------
 2 files changed, 95 insertions(+), 96 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 6a3cc46e2b..3a278820f0 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private readonly Bindable<Size> areaOffset = new BindableSize();
         private readonly Bindable<Size> areaSize = new BindableSize();
-        private readonly Bindable<Size> tabletSize = new BindableSize();
+        private readonly IBindable<Size> tabletSize = new BindableSize();
 
         private OsuSpriteText tabletName;
 
@@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 checkBounds();
             }, true);
 
-            ((IBindable<Size>)tabletSize).BindTo(handler.TabletSize);
+            tabletSize.BindTo(handler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
                 tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
@@ -123,6 +123,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             var size = tabletSize.Value;
 
+            if (size == System.Drawing.Size.Empty)
+                return;
+
             float fitX = size.Width / DrawWidth;
             float fitY = size.Height / DrawHeight;
 
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index e94df7dc1b..3f8723025f 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private readonly BindableSize areaOffset = new BindableSize();
         private readonly BindableSize areaSize = new BindableSize();
-        private readonly BindableSize tabletSize = new BindableSize();
+        private readonly IBindable<Size> tabletSize = new BindableSize();
 
         private readonly BindableNumber<int> offsetX = new BindableNumber<int> { MinValue = 0 };
         private readonly BindableNumber<int> offsetY = new BindableNumber<int> { MinValue = 0 };
@@ -54,99 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         [BackgroundDependencyLoader]
         private void load()
         {
-            areaOffset.BindTo(tabletHandler.AreaOffset);
-            areaOffset.BindValueChanged(val =>
-            {
-                offsetX.Value = val.NewValue.Width;
-                offsetY.Value = val.NewValue.Height;
-            }, true);
-
-            offsetX.BindValueChanged(val => areaOffset.Value = new Size(val.NewValue, areaOffset.Value.Height));
-            offsetY.BindValueChanged(val => areaOffset.Value = new Size(areaOffset.Value.Width, val.NewValue));
-
-            areaSize.BindTo(tabletHandler.AreaSize);
-            areaSize.BindValueChanged(val =>
-            {
-                sizeX.Value = val.NewValue.Width;
-                sizeY.Value = val.NewValue.Height;
-
-                aspectRatioApplication?.Cancel();
-                aspectRatioApplication = Schedule(() => applyAspectRatio(val));
-            }, true);
-
-            sizeX.BindValueChanged(val => areaSize.Value = new Size(val.NewValue, areaSize.Value.Height));
-            sizeY.BindValueChanged(val => areaSize.Value = new Size(areaSize.Value.Width, val.NewValue));
-
-            aspectRatio.BindValueChanged(aspect =>
-            {
-                aspectRatioApplication?.Cancel();
-                aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
-            });
-
-            ((IBindable<Size>)tabletSize).BindTo(tabletHandler.TabletSize);
-            tabletSize.BindValueChanged(val =>
-            {
-                if (tabletSize.Value == System.Drawing.Size.Empty)
-                    return;
-
-                // todo: these should propagate from a TabletChanged event or similar.
-                offsetX.MaxValue = val.NewValue.Width;
-                sizeX.Default = sizeX.MaxValue = val.NewValue.Width;
-
-                offsetY.MaxValue = val.NewValue.Height;
-                sizeY.Default = sizeY.MaxValue = val.NewValue.Height;
-
-                areaSize.Default = new Size(sizeX.Default, sizeY.Default);
-
-                updateDisplay();
-            }, true);
-        }
-
-        private float curentAspectRatio => (float)sizeX.Value / sizeY.Value;
-
-        private void applyAspectRatio(ValueChangedEvent<Size> sizeChanged)
-        {
-            float proposedAspectRatio = curentAspectRatio;
-
-            try
-            {
-                if (!aspectLock.Value)
-                {
-                    // aspect ratio was in a valid range.
-                    if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
-                    {
-                        updateAspectRatio();
-                        return;
-                    }
-                }
-
-                if (sizeChanged.NewValue.Width != sizeChanged.OldValue.Width)
-                {
-                    areaSize.Value = new Size(areaSize.Value.Width, (int)(areaSize.Value.Width / aspectRatio.Value));
-                }
-                else
-                {
-                    areaSize.Value = new Size((int)(areaSize.Value.Height * aspectRatio.Value), areaSize.Value.Height);
-                }
-            }
-            finally
-            {
-                // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
-                // this avoids a potential feedback loop.
-                aspectRatioApplication?.Cancel();
-            }
-        }
-
-        private void updateAspectRatio()
-        {
-            aspectRatio.Value = curentAspectRatio;
-        }
-
-        private void updateDisplay()
-        {
-            if (Children.Count > 0)
-                return;
-
             Children = new Drawable[]
             {
                 new TabletAreaSelection(tabletHandler)
@@ -204,6 +111,91 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     Current = sizeY
                 },
             };
+
+            areaOffset.BindTo(tabletHandler.AreaOffset);
+            areaOffset.BindValueChanged(val =>
+            {
+                offsetX.Value = val.NewValue.Width;
+                offsetY.Value = val.NewValue.Height;
+            }, true);
+
+            offsetX.BindValueChanged(val => areaOffset.Value = new Size(val.NewValue, areaOffset.Value.Height));
+            offsetY.BindValueChanged(val => areaOffset.Value = new Size(areaOffset.Value.Width, val.NewValue));
+
+            areaSize.BindTo(tabletHandler.AreaSize);
+            areaSize.BindValueChanged(val =>
+            {
+                sizeX.Value = val.NewValue.Width;
+                sizeY.Value = val.NewValue.Height;
+            }, true);
+
+            sizeX.BindValueChanged(val =>
+            {
+                areaSize.Value = new Size(val.NewValue, areaSize.Value.Height);
+
+                aspectRatioApplication?.Cancel();
+                aspectRatioApplication = Schedule(() => applyAspectRatio(sizeX));
+            });
+
+            sizeY.BindValueChanged(val =>
+            {
+                areaSize.Value = new Size(areaSize.Value.Width, val.NewValue);
+
+                aspectRatioApplication?.Cancel();
+                aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY));
+            });
+
+            aspectRatio.BindValueChanged(aspect =>
+            {
+                aspectRatioApplication?.Cancel();
+                aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
+            });
+
+            tabletSize.BindTo(tabletHandler.TabletSize);
+            tabletSize.BindValueChanged(val =>
+            {
+                if (tabletSize.Value == System.Drawing.Size.Empty)
+                    return;
+
+                // todo: these should propagate from a TabletChanged event or similar.
+                offsetX.MaxValue = val.NewValue.Width;
+                sizeX.Default = sizeX.MaxValue = val.NewValue.Width;
+
+                offsetY.MaxValue = val.NewValue.Height;
+                sizeY.Default = sizeY.MaxValue = val.NewValue.Height;
+
+                areaSize.Default = new Size(sizeX.Default, sizeY.Default);
+            }, true);
+        }
+
+        private void applyAspectRatio(BindableNumber<int> sizeChanged)
+        {
+            try
+            {
+                if (!aspectLock.Value)
+                {
+                    float proposedAspectRatio = curentAspectRatio;
+
+                    if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
+                    {
+                        // aspect ratio was in a valid range.
+                        updateAspectRatio();
+                        return;
+                    }
+                }
+
+                // if lock is applied (or the specified values were out of range) aim to adjust the axis the user was not adjusting to conform.
+                if (sizeChanged == sizeX)
+                    sizeY.Value = (int)(areaSize.Value.Width / aspectRatio.Value);
+                else
+                    sizeX.Value = (int)(areaSize.Value.Height * aspectRatio.Value);
+            }
+            finally
+            {
+                // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
+                // this avoids a potential feedback loop.
+                aspectRatioApplication?.Cancel();
+            }
         }
 
         private void forceAspectRatio(float aspectRatio)
@@ -222,5 +214,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             aspectRatioApplication?.Cancel();
             aspectLock.Value = true;
         }
+
+        private void updateAspectRatio() => aspectRatio.Value = curentAspectRatio;
+
+        private float curentAspectRatio => (float)sizeX.Value / sizeY.Value;
     }
 }

From a8e319a320a1aae74a29432b0794a16ce869330a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 18:51:43 +0900
Subject: [PATCH 304/434] Remove min/max from test scene to fix weirdness when
 switching test sizings

---
 osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 3d65db9420..aaf2f13953 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -21,12 +21,7 @@ namespace osu.Game.Tests.Visual.Settings
         {
             var tabletHandler = new TestTabletHandler();
 
-            tabletHandler.AreaOffset.MinValue = new Size(0, 0);
-            tabletHandler.AreaOffset.MaxValue = new Size(160, 100);
             tabletHandler.AreaOffset.Value = new Size(10, 10);
-
-            tabletHandler.AreaSize.MinValue = new Size(10, 10);
-            tabletHandler.AreaSize.MaxValue = new Size(160, 100);
             tabletHandler.AreaSize.Value = new Size(100, 80);
 
             AddRange(new Drawable[]

From 9a6a0f3df5e8a2d4a67f4bc567ba3e5db8a76a2c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 23:47:08 +0900
Subject: [PATCH 305/434] Add test coverage and better UI handling of no tablet
 connected scenario

---
 .../Settings/TestSceneTabletSettings.cs       |  10 +-
 .../Settings/Sections/Input/TabletSettings.cs | 130 +++++++++++-------
 2 files changed, 89 insertions(+), 51 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index aaf2f13953..2baeadddc0 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Handlers.Tablet;
 using osu.Framework.Platform;
+using osu.Framework.Utils;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Settings.Sections.Input;
 
@@ -39,6 +40,7 @@ namespace osu.Game.Tests.Visual.Settings
             AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Size(300, 300)));
             AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 300)));
             AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 700)));
+            AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(System.Drawing.Size.Empty));
         }
 
         public class TestTabletHandler : ITabletHandler
@@ -48,10 +50,14 @@ namespace osu.Game.Tests.Visual.Settings
             public BindableSize AreaOffset { get; } = new BindableSize();
             public BindableSize AreaSize { get; } = new BindableSize();
             public IBindable<Size> TabletSize => tabletSize;
-            public string DeviceName => "test tablet T-421";
+            public string DeviceName { get; private set; }
             public BindableBool Enabled { get; } = new BindableBool(true);
 
-            public void SetTabletSize(Size size) => tabletSize.Value = size;
+            public void SetTabletSize(Size size)
+            {
+                DeviceName = size != System.Drawing.Size.Empty ? $"test tablet T-{RNG.Next(999):000}" : string.Empty;
+                tabletSize.Value = size;
+            }
         }
     }
 }
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 3f8723025f..7da61cf192 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -5,9 +5,11 @@ using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
 using osu.Framework.Input.Handlers.Tablet;
 using osu.Framework.Platform;
 using osu.Framework.Threading;
+using osu.Game.Graphics.Sprites;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
 {
@@ -44,6 +46,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private ScheduledDelegate aspectRatioApplication;
 
+        private FillFlowContainer mainSettings;
+
+        private OsuSpriteText noTabletMessage;
+
         protected override string Header => "Tablet";
 
         public TabletSettings(ITabletHandler tabletHandler)
@@ -56,60 +62,77 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             Children = new Drawable[]
             {
-                new TabletAreaSelection(tabletHandler)
+                noTabletMessage = new OsuSpriteText
                 {
+                    Text = "No tablet detected!",
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                },
+                mainSettings = new FillFlowContainer
+                {
+                    Alpha = 0,
                     RelativeSizeAxes = Axes.X,
-                    Height = 300,
-                },
-                new DangerousSettingsButton
-                {
-                    Text = "Reset to full area",
-                    Action = () =>
+                    AutoSizeAxes = Axes.Y,
+                    Direction = FillDirection.Vertical,
+                    Children = new Drawable[]
                     {
-                        aspectLock.Value = false;
+                        new TabletAreaSelection(tabletHandler)
+                        {
+                            RelativeSizeAxes = Axes.X,
+                            Height = 300,
+                            Margin = new MarginPadding(10)
+                        },
+                        new DangerousSettingsButton
+                        {
+                            Text = "Reset to full area",
+                            Action = () =>
+                            {
+                                aspectLock.Value = false;
 
-                        areaOffset.SetDefault();
-                        areaSize.SetDefault();
-                    },
-                },
-                new SettingsButton
-                {
-                    Text = "Conform to current game aspect ratio",
-                    Action = () =>
-                    {
-                        forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
+                                areaOffset.SetDefault();
+                                areaSize.SetDefault();
+                            },
+                        },
+                        new SettingsButton
+                        {
+                            Text = "Conform to current game aspect ratio",
+                            Action = () =>
+                            {
+                                forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
+                            }
+                        },
+                        new SettingsSlider<float>
+                        {
+                            LabelText = "Aspect Ratio",
+                            Current = aspectRatio
+                        },
+                        new SettingsSlider<int>
+                        {
+                            LabelText = "X Offset",
+                            Current = offsetX
+                        },
+                        new SettingsSlider<int>
+                        {
+                            LabelText = "Y Offset",
+                            Current = offsetY
+                        },
+                        new SettingsCheckbox
+                        {
+                            LabelText = "Lock aspect ratio",
+                            Current = aspectLock
+                        },
+                        new SettingsSlider<int>
+                        {
+                            LabelText = "Width",
+                            Current = sizeX
+                        },
+                        new SettingsSlider<int>
+                        {
+                            LabelText = "Height",
+                            Current = sizeY
+                        },
                     }
                 },
-                new SettingsSlider<float>
-                {
-                    LabelText = "Aspect Ratio",
-                    Current = aspectRatio
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "X Offset",
-                    Current = offsetX
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Y Offset",
-                    Current = offsetY
-                },
-                new SettingsCheckbox
-                {
-                    LabelText = "Lock aspect ratio",
-                    Current = aspectLock
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Width",
-                    Current = sizeX
-                },
-                new SettingsSlider<int>
-                {
-                    LabelText = "Height",
-                    Current = sizeY
-                },
             };
 
             areaOffset.BindTo(tabletHandler.AreaOffset);
@@ -154,8 +177,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             tabletSize.BindTo(tabletHandler.TabletSize);
             tabletSize.BindValueChanged(val =>
             {
-                if (tabletSize.Value == System.Drawing.Size.Empty)
+                bool tabletFound = tabletSize.Value != System.Drawing.Size.Empty;
+
+                if (!tabletFound)
+                {
+                    mainSettings.Hide();
+                    noTabletMessage.Show();
                     return;
+                }
+
+                mainSettings.Show();
+                noTabletMessage.Hide();
 
                 // todo: these should propagate from a TabletChanged event or similar.
                 offsetX.MaxValue = val.NewValue.Width;

From d422a6590036dc26a881fa2519da4d09111ac479 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 23:47:18 +0900
Subject: [PATCH 306/434] Fix initial tablet size not being initialised

---
 .../Settings/Sections/Input/TabletAreaSelection.cs         | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 3a278820f0..af144c8102 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -80,6 +80,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     },
                 }
             };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
 
             areaOffset.BindTo(handler.AreaOffset);
             areaOffset.BindValueChanged(val =>
@@ -101,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
                 tabletName.Text = handler.DeviceName;
                 checkBounds();
-            });
+            }, true);
         }
 
         [Resolved]

From 9d0c8902a6e6dcc1edc369b8ebf768089f22c8a6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 16 Mar 2021 23:57:05 +0900
Subject: [PATCH 307/434] Fix margins and spacing between sub flowed items

---
 .../Overlays/Settings/Sections/Input/TabletAreaSelection.cs | 6 +++---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 4 +++-
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index af144c8102..3b1bae7cf0 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -32,13 +32,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         public TabletAreaSelection(ITabletHandler handler)
         {
             this.handler = handler;
+
+            Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
         }
 
         [BackgroundDependencyLoader]
         private void load()
         {
-            Padding = new MarginPadding(5);
-
             InternalChild = tabletContainer = new Container
             {
                 Anchor = Anchor.Centre,
@@ -131,7 +131,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             if (size == System.Drawing.Size.Empty)
                 return;
 
-            float fitX = size.Width / DrawWidth;
+            float fitX = size.Width / (DrawWidth - Padding.Left - Padding.Right);
             float fitY = size.Height / DrawHeight;
 
             float adjust = MathF.Max(fitX, fitY);
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 7da61cf192..b17cfced95 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -10,6 +10,7 @@ using osu.Framework.Input.Handlers.Tablet;
 using osu.Framework.Platform;
 using osu.Framework.Threading;
 using osu.Game.Graphics.Sprites;
+using osuTK;
 
 namespace osu.Game.Overlays.Settings.Sections.Input
 {
@@ -67,12 +68,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     Text = "No tablet detected!",
                     Anchor = Anchor.Centre,
                     Origin = Anchor.Centre,
+                    Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }
                 },
                 mainSettings = new FillFlowContainer
                 {
                     Alpha = 0,
                     RelativeSizeAxes = Axes.X,
                     AutoSizeAxes = Axes.Y,
+                    Spacing = new Vector2(0, 8),
                     Direction = FillDirection.Vertical,
                     Children = new Drawable[]
                     {
@@ -80,7 +83,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                         {
                             RelativeSizeAxes = Axes.X,
                             Height = 300,
-                            Margin = new MarginPadding(10)
                         },
                         new DangerousSettingsButton
                         {

From 196f95ae545853c00e3da66ba9bc20e4871c75a2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 12:50:02 +0900
Subject: [PATCH 308/434] Update to use new bindables and centered area offset

---
 .../Settings/TestSceneTabletSettings.cs       | 44 +++++++-----
 .../Sections/Input/TabletAreaSelection.cs     | 49 ++++++++------
 .../Settings/Sections/Input/TabletSettings.cs | 67 ++++++++++---------
 3 files changed, 89 insertions(+), 71 deletions(-)

diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 2baeadddc0..a7f6c8c0d3 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Drawing;
 using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -11,6 +10,7 @@ using osu.Framework.Platform;
 using osu.Framework.Utils;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Settings.Sections.Input;
+using osuTK;
 
 namespace osu.Game.Tests.Visual.Settings
 {
@@ -22,9 +22,6 @@ namespace osu.Game.Tests.Visual.Settings
         {
             var tabletHandler = new TestTabletHandler();
 
-            tabletHandler.AreaOffset.Value = new Size(10, 10);
-            tabletHandler.AreaSize.Value = new Size(100, 80);
-
             AddRange(new Drawable[]
             {
                 new TabletSettings(tabletHandler)
@@ -36,27 +33,40 @@ namespace osu.Game.Tests.Visual.Settings
                 }
             });
 
-            AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Size(160, 100)));
-            AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Size(300, 300)));
-            AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 300)));
-            AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Size(100, 700)));
-            AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(System.Drawing.Size.Empty));
+            AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
+            AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
+            AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
+            AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 700)));
+            AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
         }
 
         public class TestTabletHandler : ITabletHandler
         {
-            private readonly Bindable<Size> tabletSize = new Bindable<Size>();
+            public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
+            public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
+
+            public IBindable<TabletInfo> Tablet => tablet;
+
+            private readonly Bindable<TabletInfo> tablet = new Bindable<TabletInfo>();
 
-            public BindableSize AreaOffset { get; } = new BindableSize();
-            public BindableSize AreaSize { get; } = new BindableSize();
-            public IBindable<Size> TabletSize => tabletSize;
-            public string DeviceName { get; private set; }
             public BindableBool Enabled { get; } = new BindableBool(true);
 
-            public void SetTabletSize(Size size)
+            public void SetTabletSize(Vector2 size)
             {
-                DeviceName = size != System.Drawing.Size.Empty ? $"test tablet T-{RNG.Next(999):000}" : string.Empty;
-                tabletSize.Value = size;
+                tablet.Value = size != Vector2.Zero ? new TabletInfo($"test tablet T-{RNG.Next(999):000}", size) : null;
+
+                AreaSize.Default = new Vector2(size.X, size.Y);
+
+                // if it's clear the user has not configured the area, take the full area from the tablet that was just found.
+                if (AreaSize.Value == Vector2.Zero)
+                    AreaSize.SetDefault();
+
+                AreaOffset.Default = new Vector2(size.X / 2, size.Y / 2);
+
+                // likewise with the position, use the centre point if it has not been configured.
+                // it's safe to assume no user would set their centre point to 0,0 for now.
+                if (AreaOffset.Value == Vector2.Zero)
+                    AreaOffset.SetDefault();
             }
         }
     }
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 3b1bae7cf0..c0412fb99d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -23,9 +22,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         private Container tabletContainer;
         private Container usableAreaContainer;
 
-        private readonly Bindable<Size> areaOffset = new BindableSize();
-        private readonly Bindable<Size> areaSize = new BindableSize();
-        private readonly IBindable<Size> tabletSize = new BindableSize();
+        private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>();
+        private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>();
+
+        private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>();
 
         private OsuSpriteText tabletName;
 
@@ -56,6 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     },
                     usableAreaContainer = new Container
                     {
+                        Origin = Anchor.Centre,
                         Children = new Drawable[]
                         {
                             new Box
@@ -89,24 +90,27 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             areaOffset.BindTo(handler.AreaOffset);
             areaOffset.BindValueChanged(val =>
             {
-                usableAreaContainer.MoveTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
-                checkBounds();
+                usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint)
+                                   .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
             }, true);
 
             areaSize.BindTo(handler.AreaSize);
             areaSize.BindValueChanged(val =>
             {
-                usableAreaContainer.ResizeTo(new Vector2(val.NewValue.Width, val.NewValue.Height), 100, Easing.OutQuint);
+                usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
+                                   .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
+            }, true);
+
+            tablet.BindTo(handler.Tablet);
+            tablet.BindValueChanged(val =>
+            {
+                tabletContainer.Size = val.NewValue?.Size ?? Vector2.Zero;
+                tabletName.Text = val.NewValue?.Name ?? string.Empty;
                 checkBounds();
             }, true);
 
-            tabletSize.BindTo(handler.TabletSize);
-            tabletSize.BindValueChanged(val =>
-            {
-                tabletContainer.Size = new Vector2(val.NewValue.Width, val.NewValue.Height);
-                tabletName.Text = handler.DeviceName;
-                checkBounds();
-            }, true);
+            // initial animation should be instant.
+            FinishTransforms(true);
         }
 
         [Resolved]
@@ -114,10 +118,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private void checkBounds()
         {
-            Size areaExtent = areaOffset.Value + areaSize.Value;
+            if (tablet.Value == null)
+                return;
 
-            bool isWithinBounds = areaExtent.Width <= tabletSize.Value.Width
-                                  && areaExtent.Height <= tabletSize.Value.Height;
+            var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad;
+
+            bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft) &&
+                                  tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight);
 
             usableAreaContainer.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
         }
@@ -126,13 +133,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             base.Update();
 
-            var size = tabletSize.Value;
-
-            if (size == System.Drawing.Size.Empty)
+            if (!(tablet.Value?.Size is Vector2 size))
                 return;
 
-            float fitX = size.Width / (DrawWidth - Padding.Left - Padding.Right);
-            float fitY = size.Height / DrawHeight;
+            float fitX = size.X / (DrawWidth - Padding.Left - Padding.Right);
+            float fitY = size.Y / DrawHeight;
 
             float adjust = MathF.Max(fitX, fitY);
 
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index b17cfced95..9f81391434 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Drawing;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -18,15 +17,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
     {
         private readonly ITabletHandler tabletHandler;
 
-        private readonly BindableSize areaOffset = new BindableSize();
-        private readonly BindableSize areaSize = new BindableSize();
-        private readonly IBindable<Size> tabletSize = new BindableSize();
+        private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>();
+        private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>();
+        private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>();
 
-        private readonly BindableNumber<int> offsetX = new BindableNumber<int> { MinValue = 0 };
-        private readonly BindableNumber<int> offsetY = new BindableNumber<int> { MinValue = 0 };
+        private readonly BindableNumber<float> offsetX = new BindableNumber<float> { MinValue = 0 };
+        private readonly BindableNumber<float> offsetY = new BindableNumber<float> { MinValue = 0 };
 
-        private readonly BindableNumber<int> sizeX = new BindableNumber<int> { MinValue = 10 };
-        private readonly BindableNumber<int> sizeY = new BindableNumber<int> { MinValue = 10 };
+        private readonly BindableNumber<float> sizeX = new BindableNumber<float> { MinValue = 10 };
+        private readonly BindableNumber<float> sizeY = new BindableNumber<float> { MinValue = 10 };
 
         [Resolved]
         private GameHost host { get; set; }
@@ -108,12 +107,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                             LabelText = "Aspect Ratio",
                             Current = aspectRatio
                         },
-                        new SettingsSlider<int>
+                        new SettingsSlider<float>
                         {
                             LabelText = "X Offset",
                             Current = offsetX
                         },
-                        new SettingsSlider<int>
+                        new SettingsSlider<float>
                         {
                             LabelText = "Y Offset",
                             Current = offsetY
@@ -123,12 +122,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                             LabelText = "Lock aspect ratio",
                             Current = aspectLock
                         },
-                        new SettingsSlider<int>
+                        new SettingsSlider<float>
                         {
                             LabelText = "Width",
                             Current = sizeX
                         },
-                        new SettingsSlider<int>
+                        new SettingsSlider<float>
                         {
                             LabelText = "Height",
                             Current = sizeY
@@ -140,23 +139,23 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             areaOffset.BindTo(tabletHandler.AreaOffset);
             areaOffset.BindValueChanged(val =>
             {
-                offsetX.Value = val.NewValue.Width;
-                offsetY.Value = val.NewValue.Height;
+                offsetX.Value = val.NewValue.X;
+                offsetY.Value = val.NewValue.Y;
             }, true);
 
-            offsetX.BindValueChanged(val => areaOffset.Value = new Size(val.NewValue, areaOffset.Value.Height));
-            offsetY.BindValueChanged(val => areaOffset.Value = new Size(areaOffset.Value.Width, val.NewValue));
+            offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y));
+            offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue));
 
             areaSize.BindTo(tabletHandler.AreaSize);
             areaSize.BindValueChanged(val =>
             {
-                sizeX.Value = val.NewValue.Width;
-                sizeY.Value = val.NewValue.Height;
+                sizeX.Value = val.NewValue.X;
+                sizeY.Value = val.NewValue.Y;
             }, true);
 
             sizeX.BindValueChanged(val =>
             {
-                areaSize.Value = new Size(val.NewValue, areaSize.Value.Height);
+                areaSize.Value = new Vector2(val.NewValue, areaSize.Value.Y);
 
                 aspectRatioApplication?.Cancel();
                 aspectRatioApplication = Schedule(() => applyAspectRatio(sizeX));
@@ -164,7 +163,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             sizeY.BindValueChanged(val =>
             {
-                areaSize.Value = new Size(areaSize.Value.Width, val.NewValue);
+                areaSize.Value = new Vector2(areaSize.Value.X, val.NewValue);
 
                 aspectRatioApplication?.Cancel();
                 aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY));
@@ -176,10 +175,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
             });
 
-            tabletSize.BindTo(tabletHandler.TabletSize);
-            tabletSize.BindValueChanged(val =>
+            tablet.BindTo(tabletHandler.Tablet);
+            tablet.BindValueChanged(val =>
             {
-                bool tabletFound = tabletSize.Value != System.Drawing.Size.Empty;
+                var tab = val.NewValue;
+
+                bool tabletFound = tab != null;
 
                 if (!tabletFound)
                 {
@@ -192,17 +193,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 noTabletMessage.Hide();
 
                 // todo: these should propagate from a TabletChanged event or similar.
-                offsetX.MaxValue = val.NewValue.Width;
-                sizeX.Default = sizeX.MaxValue = val.NewValue.Width;
+                offsetX.MaxValue = tab.Size.X;
+                offsetX.Default = tab.Size.X / 2;
+                sizeX.Default = sizeX.MaxValue = tab.Size.X;
 
-                offsetY.MaxValue = val.NewValue.Height;
-                sizeY.Default = sizeY.MaxValue = val.NewValue.Height;
+                offsetY.MaxValue = tab.Size.Y;
+                offsetY.Default = tab.Size.Y / 2;
+                sizeY.Default = sizeY.MaxValue = tab.Size.Y;
 
-                areaSize.Default = new Size(sizeX.Default, sizeY.Default);
+                areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
             }, true);
         }
 
-        private void applyAspectRatio(BindableNumber<int> sizeChanged)
+        private void applyAspectRatio(BindableNumber<float> sizeChanged)
         {
             try
             {
@@ -220,9 +223,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
                 // if lock is applied (or the specified values were out of range) aim to adjust the axis the user was not adjusting to conform.
                 if (sizeChanged == sizeX)
-                    sizeY.Value = (int)(areaSize.Value.Width / aspectRatio.Value);
+                    sizeY.Value = (int)(areaSize.Value.X / aspectRatio.Value);
                 else
-                    sizeX.Value = (int)(areaSize.Value.Height * aspectRatio.Value);
+                    sizeX.Value = (int)(areaSize.Value.Y * aspectRatio.Value);
             }
             finally
             {
@@ -251,6 +254,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private void updateAspectRatio() => aspectRatio.Value = curentAspectRatio;
 
-        private float curentAspectRatio => (float)sizeX.Value / sizeY.Value;
+        private float curentAspectRatio => sizeX.Value / sizeY.Value;
     }
 }

From fb7d095e4a0a934664db0a5cbc7585eea55b46da Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 13:03:06 +0900
Subject: [PATCH 309/434] Show aspect ratio for current usable area

---
 .../Sections/Input/TabletAreaSelection.cs     | 30 +++++++++++++++----
 1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index c0412fb99d..ba219cfe7d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -29,6 +29,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
         private OsuSpriteText tabletName;
 
+        private Box usableFill;
+        private OsuSpriteText usableAreaText;
+
         public TabletAreaSelection(ITabletHandler handler)
         {
             this.handler = handler;
@@ -59,17 +62,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                         Origin = Anchor.Centre,
                         Children = new Drawable[]
                         {
-                            new Box
+                            usableFill = new Box
                             {
                                 RelativeSizeAxes = Axes.Both,
                                 Alpha = 0.6f,
                             },
-                            new OsuSpriteText
+                            usableAreaText = new OsuSpriteText
                             {
-                                Text = "usable area",
                                 Anchor = Anchor.Centre,
                                 Origin = Anchor.Centre,
-                                Colour = Color4.Black,
+                                Colour = Color4.White,
                                 Font = OsuFont.Default.With(size: 12)
                             }
                         }
@@ -99,6 +101,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             {
                 usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
                                    .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
+
+                int x = (int)val.NewValue.X;
+                int y = (int)val.NewValue.Y;
+                int commonDivider = greatestCommonDivider(x, y);
+
+                usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
             }, true);
 
             tablet.BindTo(handler.Tablet);
@@ -113,6 +121,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             FinishTransforms(true);
         }
 
+        private static int greatestCommonDivider(int a, int b)
+        {
+            while (b != 0)
+            {
+                int remainder = a % b;
+                a = b;
+                b = remainder;
+            }
+
+            return a;
+        }
+
         [Resolved]
         private OsuColour colour { get; set; }
 
@@ -126,7 +146,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft) &&
                                   tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight);
 
-            usableAreaContainer.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
+            usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
         }
 
         protected override void Update()

From e8c20bdcb12a3a1f513a6fa63f72511630799afe Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 13:05:43 +0900
Subject: [PATCH 310/434] Add centre crosshair

---
 .../Sections/Input/TabletAreaSelection.cs       | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index ba219cfe7d..0a44b1a44d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -67,12 +67,27 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                                 RelativeSizeAxes = Axes.Both,
                                 Alpha = 0.6f,
                             },
+                            new Box
+                            {
+                                Colour = Color4.White,
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                                Height = 5,
+                            },
+                            new Box
+                            {
+                                Colour = Color4.White,
+                                Anchor = Anchor.Centre,
+                                Origin = Anchor.Centre,
+                                Width = 5,
+                            },
                             usableAreaText = new OsuSpriteText
                             {
                                 Anchor = Anchor.Centre,
                                 Origin = Anchor.Centre,
                                 Colour = Color4.White,
-                                Font = OsuFont.Default.With(size: 12)
+                                Font = OsuFont.Default.With(size: 12),
+                                Y = 10
                             }
                         }
                     },

From 6285dcd1a1d9989dd9daeeb685102b48472e4ba6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 13:09:15 +0900
Subject: [PATCH 311/434] Add arbitrary value to fix FP contains check failures

---
 .../Overlays/Settings/Sections/Input/TabletAreaSelection.cs   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index 0a44b1a44d..df25668411 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -158,8 +158,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
 
             var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad;
 
-            bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft) &&
-                                  tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight);
+            bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) &&
+                                  tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
 
             usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
         }

From c624aa939774cc3f783eb2a588d96c8afefc37f2 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 13:23:23 +0900
Subject: [PATCH 312/434] Only update tablet values on commit

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 9f81391434..b2d37e345f 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -104,16 +104,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                         },
                         new SettingsSlider<float>
                         {
+                            TransferValueOnCommit = true,
                             LabelText = "Aspect Ratio",
                             Current = aspectRatio
                         },
                         new SettingsSlider<float>
                         {
+                            TransferValueOnCommit = true,
                             LabelText = "X Offset",
                             Current = offsetX
                         },
                         new SettingsSlider<float>
                         {
+                            TransferValueOnCommit = true,
                             LabelText = "Y Offset",
                             Current = offsetY
                         },
@@ -124,11 +127,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                         },
                         new SettingsSlider<float>
                         {
+                            TransferValueOnCommit = true,
                             LabelText = "Width",
                             Current = sizeX
                         },
                         new SettingsSlider<float>
                         {
+                            TransferValueOnCommit = true,
                             LabelText = "Height",
                             Current = sizeY
                         },

From b1c4ac9f42842943d451f13b57da44231b34431d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 14:13:53 +0900
Subject: [PATCH 313/434] Remove local implementation of Vector2Converter

This has been moved to framework in
https://github.com/ppy/osu-framework/pull/4285.
---
 .../Converters/Vector2Converter.cs            | 34 -------------------
 .../IO/Serialization/IJsonSerializable.cs     |  2 --
 2 files changed, 36 deletions(-)
 delete mode 100644 osu.Game/IO/Serialization/Converters/Vector2Converter.cs

diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
deleted file mode 100644
index 46447b607b..0000000000
--- a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using osuTK;
-
-namespace osu.Game.IO.Serialization.Converters
-{
-    /// <summary>
-    /// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
-    /// </summary>
-    public class Vector2Converter : JsonConverter<Vector2>
-    {
-        public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
-        {
-            var obj = JObject.Load(reader);
-            return new Vector2((float)obj["x"], (float)obj["y"]);
-        }
-
-        public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
-        {
-            writer.WriteStartObject();
-
-            writer.WritePropertyName("x");
-            writer.WriteValue(value.X);
-            writer.WritePropertyName("y");
-            writer.WriteValue(value.Y);
-
-            writer.WriteEndObject();
-        }
-    }
-}
diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index ac95d47c4b..30430e6f7f 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using Newtonsoft.Json;
-using osu.Game.IO.Serialization.Converters;
 
 namespace osu.Game.IO.Serialization
 {
@@ -28,7 +27,6 @@ namespace osu.Game.IO.Serialization
             Formatting = Formatting.Indented,
             ObjectCreationHandling = ObjectCreationHandling.Replace,
             DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
-            Converters = new JsonConverter[] { new Vector2Converter() },
             ContractResolver = new KeyContractResolver()
         };
     }

From 1e82033c840725d4facd32a95be7d7ebe682e908 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 14:18:56 +0900
Subject: [PATCH 314/434] Move bindings to LoadComplete to avoid cross-thread
 issues

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index b2d37e345f..19a3c5b6f2 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -140,6 +140,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                     }
                 },
             };
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
 
             areaOffset.BindTo(tabletHandler.AreaOffset);
             areaOffset.BindValueChanged(val =>

From fefb0078056fcfada6602ca1396318b618767bde Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 17 Mar 2021 14:19:15 +0900
Subject: [PATCH 315/434] Remove no longer relevant comment

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 19a3c5b6f2..d0e3ddbd91 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -202,7 +202,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 mainSettings.Show();
                 noTabletMessage.Hide();
 
-                // todo: these should propagate from a TabletChanged event or similar.
                 offsetX.MaxValue = tab.Size.X;
                 offsetX.Default = tab.Size.X / 2;
                 sizeX.Default = sizeX.MaxValue = tab.Size.X;

From bd1e2da1c2260457234bf40d16af2f130f68765a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 19:09:39 +0900
Subject: [PATCH 316/434] Always hide other overlays, even if the new one is
 not loaded

---
 osu.Game/OsuGame.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index e5e1f6946e..e2f0f0c05b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -756,12 +756,12 @@ namespace osu.Game
 
         private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
         {
+            otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
+
             // generally shouldn't ever hit this state, but protects against a crash on attempting to change ChildDepth.
             if (overlay.LoadState < LoadState.Ready)
                 return;
 
-            otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
-
             // show above others if not visible at all, else leave at current depth.
             if (!overlay.IsPresent)
                 overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);

From c0c8b3e46c49290709a8c9948693c72857404704 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 19:22:47 +0900
Subject: [PATCH 317/434] Fix regression meaning `SkinnableSound`
 initialisation may never happen

---
 osu.Game/Skinning/SkinnableSound.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index e447f9c44c..9c6a4f7970 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -131,6 +131,14 @@ namespace osu.Game.Skinning
             });
         }
 
+        protected override void LoadAsyncComplete()
+        {
+            base.LoadAsyncComplete();
+
+            if (!samplesContainer.Any())
+                updateSamples();
+        }
+
         /// <summary>
         /// Stops the samples.
         /// </summary>

From 9be7981e0d981cea5ed9b605ab9423e77877b7f7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 19:45:00 +0900
Subject: [PATCH 318/434] Adjust timeline ticks to be more visible

---
 osu.Game/Screens/Edit/BindableBeatDivisor.cs        |  3 +--
 .../Edit/Compose/Components/DistanceSnapGrid.cs     |  2 +-
 .../Components/Timeline/TimelineTickDisplay.cs      | 13 +++++++++----
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index d9477dd4bc..ff33f0c70d 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -4,7 +4,6 @@
 using System;
 using System.Linq;
 using osu.Framework.Bindables;
-using osu.Framework.Graphics.Colour;
 using osu.Game.Graphics;
 using osuTK.Graphics;
 
@@ -48,7 +47,7 @@ namespace osu.Game.Screens.Edit
         /// <param name="beatDivisor">The beat divisor.</param>
         /// <param name="colours">The set of colours.</param>
         /// <returns>The applicable colour from <paramref name="colours"/> for <paramref name="beatDivisor"/>.</returns>
-        public static ColourInfo GetColourFor(int beatDivisor, OsuColour colours)
+        public static Color4 GetColourFor(int beatDivisor, OsuColour colours)
         {
             switch (beatDivisor)
             {
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 8a92a2011d..59f88ac641 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
             var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);
 
             int repeatIndex = placementIndex / beatDivisor.Value;
-            return colour.MultiplyAlpha(0.5f / (repeatIndex + 1));
+            return ColourInfo.SingleColour(colour).MultiplyAlpha(0.5f / (repeatIndex + 1));
         }
     }
 }
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
index fb11b859a7..c070c833f8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
@@ -6,7 +6,9 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Caching;
+using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics;
 using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -124,25 +126,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
                         if (beat == 0 && i == 0)
                             nextMinTick = float.MinValue;
 
-                        var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
+                        int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
 
                         var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
                         var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
 
+                        bool isMainBeat = indexInBar == 0;
+
                         // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
-                        var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
+                        float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f;
+                        float gradientOpacity = isMainBeat ? 1 : 0;
 
                         var topPoint = getNextUsablePoint();
                         topPoint.X = xPos;
-                        topPoint.Colour = colour;
                         topPoint.Height = height;
+                        topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity));
                         topPoint.Anchor = Anchor.TopLeft;
                         topPoint.Origin = Anchor.TopCentre;
 
                         var bottomPoint = getNextUsablePoint();
                         bottomPoint.X = xPos;
-                        bottomPoint.Colour = colour;
                         bottomPoint.Anchor = Anchor.BottomLeft;
+                        bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour);
                         bottomPoint.Origin = Anchor.BottomCentre;
                         bottomPoint.Height = height;
                     }

From 8955071703d40ed9f0210a45def45546bf1c19cb Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:01:45 +0900
Subject: [PATCH 319/434] Change editor speed adjust to adjust frequency

---
 osu.Game/Screens/Edit/Components/PlaybackControl.cs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 9739f2876a..bdc6e238c8 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Components
         [Resolved]
         private EditorClock editorClock { get; set; }
 
-        private readonly BindableNumber<double> tempo = new BindableDouble(1);
+        private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
 
         [BackgroundDependencyLoader]
         private void load()
@@ -58,16 +58,16 @@ namespace osu.Game.Screens.Edit.Components
                     RelativeSizeAxes = Axes.Both,
                     Height = 0.5f,
                     Padding = new MarginPadding { Left = 45 },
-                    Child = new PlaybackTabControl { Current = tempo },
+                    Child = new PlaybackTabControl { Current = freqAdjust },
                 }
             };
 
-            Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
+            Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true);
         }
 
         protected override void Dispose(bool isDisposing)
         {
-            Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
+            Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust);
 
             base.Dispose(isDisposing);
         }
@@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Components
 
         private class PlaybackTabControl : OsuTabControl<double>
         {
-            private static readonly double[] tempo_values = { 0.5, 0.75, 1 };
+            private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };
 
             protected override TabItem<double> CreateTabItem(double value) => new PlaybackTabItem(value);
 

From 86b229b1c94a1e0e3379b4e013206fd5ac996771 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:05:18 +0900
Subject: [PATCH 320/434] Increase maximum usable aspect ratio to account for
 ultrawide monitors

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index d0e3ddbd91..73ac8e4f35 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -31,9 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         private GameHost host { get; set; }
 
         /// <summary>
-        /// Based on the longest available smartphone.
+        /// Based on ultrawide monitor configurations.
         /// </summary>
-        private const float largest_feasible_aspect_ratio = 20f / 9;
+        private const float largest_feasible_aspect_ratio = 21f / 9;
 
         private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
         {

From 4795170c6044bc7092b4c78b687f5a5bdc880088 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:07:13 +0900
Subject: [PATCH 321/434] Add back the default json converter locally to ensure
 it's actually used

---
 osu.Game/IO/Serialization/IJsonSerializable.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index 30430e6f7f..ba188963ea 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -1,7 +1,9 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Collections.Generic;
 using Newtonsoft.Json;
+using osu.Framework.IO.Serialization;
 
 namespace osu.Game.IO.Serialization
 {
@@ -27,6 +29,7 @@ namespace osu.Game.IO.Serialization
             Formatting = Formatting.Indented,
             ObjectCreationHandling = ObjectCreationHandling.Replace,
             DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
+            Converters = new List<JsonConverter> { new Vector2Converter() },
             ContractResolver = new KeyContractResolver()
         };
     }

From 095b7f86685799249f5427ba7010f55a6a7e6646 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:09:12 +0900
Subject: [PATCH 322/434] Rewrite code to account for non-loaded edge case

---
 osu.Game/OsuGame.cs | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index e2f0f0c05b..be919d60ca 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -758,13 +758,15 @@ namespace osu.Game
         {
             otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
 
-            // generally shouldn't ever hit this state, but protects against a crash on attempting to change ChildDepth.
-            if (overlay.LoadState < LoadState.Ready)
+            // Partially visible so leave it at the current depth.
+            if (overlay.IsPresent)
                 return;
 
-            // show above others if not visible at all, else leave at current depth.
-            if (!overlay.IsPresent)
+            // Show above all other overlays.
+            if (overlay.IsLoaded)
                 overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
+            else
+                overlay.Depth = (float)-Clock.CurrentTime;
         }
 
         private void forwardLoggedErrorsToNotifications()

From 6f32c302eb692f2a8c7bf6122149a28d96672446 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:13:51 +0900
Subject: [PATCH 323/434] Add checkbox to optionally disable tablet handling

---
 .../Settings/Sections/Input/TabletSettings.cs         | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 73ac8e4f35..893fe575cd 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -62,11 +62,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
         {
             Children = new Drawable[]
             {
+                new SettingsCheckbox
+                {
+                    LabelText = "Enabled",
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
+                    Current = tabletHandler.Enabled
+                },
                 noTabletMessage = new OsuSpriteText
                 {
                     Text = "No tablet detected!",
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
                     Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }
                 },
                 mainSettings = new FillFlowContainer

From 63cbac3bd059778a8b94ad0d5e5b47beab637f33 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:15:29 +0900
Subject: [PATCH 324/434] Ensure aspect ratio slider gets an initial value

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 893fe575cd..b06b148984 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -186,6 +186,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
                 aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY));
             });
 
+            updateAspectRatio();
             aspectRatio.BindValueChanged(aspect =>
             {
                 aspectRatioApplication?.Cancel();

From b2d8db3a92d65c2db7a26d08f276a30fef82c7c7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:25:21 +0900
Subject: [PATCH 325/434] Rename incorrect variable

---
 osu.Game/Skinning/PoolableSkinnableSample.cs | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 09e087d0f2..c01a6d20cc 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -94,21 +94,21 @@ namespace osu.Game.Skinning
             sampleContainer.Clear();
             Sample = null;
 
-            var ch = CurrentSkin.GetSample(sampleInfo);
+            var sample = CurrentSkin.GetSample(sampleInfo);
 
-            if (ch == null && AllowDefaultFallback)
+            if (sample == null && AllowDefaultFallback)
             {
                 foreach (var lookup in sampleInfo.LookupNames)
                 {
-                    if ((ch = sampleStore.Get(lookup)) != null)
+                    if ((sample = sampleStore.Get(lookup)) != null)
                         break;
                 }
             }
 
-            if (ch == null)
+            if (sample == null)
                 return;
 
-            sampleContainer.Add(Sample = new DrawableSample(ch));
+            sampleContainer.Add(Sample = new DrawableSample(sample));
 
             // Start playback internally for the new sample if the previous one was playing beforehand.
             if (wasPlaying && Looping)

From 68aaf90702458f288913e191c4d27c342aa7a358 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 20:30:52 +0900
Subject: [PATCH 326/434] Fix disposal rather than performing some weird hack

---
 osu.Game/Skinning/LegacySkin.cs              | 4 +++-
 osu.Game/Skinning/PoolableSkinnableSample.cs | 8 --------
 2 files changed, 3 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index b69e99773c..ec49d43c67 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -509,7 +509,7 @@ namespace osu.Game.Skinning
         /// <summary>
         /// A sample wrapper which keeps a reference to the contained skin to avoid finalizer garbage collection of the managing SampleStore.
         /// </summary>
-        private class LegacySkinSample : ISample
+        private class LegacySkinSample : ISample, IDisposable
         {
             private readonly Sample sample;
 
@@ -575,6 +575,8 @@ namespace osu.Game.Skinning
             public IBindable<double> AggregateFrequency => sample.AggregateFrequency;
 
             public IBindable<double> AggregateTempo => sample.AggregateTempo;
+
+            public void Dispose() => sample.Dispose();
         }
     }
 }
diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index c01a6d20cc..b04158a58f 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -83,14 +83,6 @@ namespace osu.Game.Skinning
 
             bool wasPlaying = Playing;
 
-            if (activeChannel != null)
-            {
-                // when switching away from previous samples, we don't want to call Stop() on them as it sounds better to let them play out.
-                // this may change in the future if we use PoolableSkinSample in more locations than gameplay.
-                // we *do* want to turn off looping, else we end up with an infinite looping sample running in the background.
-                activeChannel.Looping = false;
-            }
-
             sampleContainer.Clear();
             Sample = null;
 

From 71a361337da8ee2ddac2feeb8af3e9644248c3d7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 19 Mar 2021 21:57:48 +0900
Subject: [PATCH 327/434] Add comment regarding usage of `Reverse()`

Co-authored-by: Dan Balasescu <smoogipoo@smgi.me>
---
 .../Compose/Components/Timeline/TimelineBlueprintContainer.cs    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 4522418e87..3526f264a7 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -138,6 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
 
             Stack<double> currentConcurrentObjects = new Stack<double>();
 
+            // Reversing is done to enumerate in order of increasing StartTime.
             foreach (var b in SelectionBlueprints.Reverse())
             {
                 while (currentConcurrentObjects.TryPeek(out double stackEndTime))

From 9634560d4b761a2a3aa90d78ffb1933e2c96c7c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Fri, 19 Mar 2021 21:36:28 +0100
Subject: [PATCH 328/434] Fix control point visualiser crashing after
 deselections

`SliderSelectionBlueprint.OnDeselected()` would expire the
`ControlPointVisualiser` on deselection, leading to its removal from the
blueprint and eventual disposal, but still kept a separate reference to
said visualiser in another field.

This could lead to that stale reference to a disposed child getting read
in `ReceivePositionalInputAt()`, crashing quite a ways down over at the
framework side on futilely trying to compute the bounding box of a
drawable with no parent.
---
 .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs         | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 3d3dff653a..befe3c6695 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -114,6 +114,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
 
             // throw away frame buffers on deselection.
             ControlPointVisualiser?.Expire();
+            ControlPointVisualiser = null;
+
             BodyPiece.RecyclePath();
         }
 

From e67c759eef36a409e4b57ab1c7643eb307280cd5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Fri, 19 Mar 2021 22:44:31 +0100
Subject: [PATCH 329/434] Mark control point visualiser as possibly-null

---
 .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs        | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index befe3c6695..ba9bb3c485 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
         protected SliderBodyPiece BodyPiece { get; private set; }
         protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
         protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
+
+        [CanBeNull]
         protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
 
         private readonly DrawableSlider slider;

From 8e0536e1e2d2eb66a7572dcf27f19d65cb4f5e7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Fri, 19 Mar 2021 22:20:26 +0100
Subject: [PATCH 330/434] Add failing test scene

---
 .../Editing/TestSceneBlueprintSelection.cs    | 70 +++++++++++++++++++
 1 file changed, 70 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs

diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
new file mode 100644
index 0000000000..fd9c09fd5f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
@@ -0,0 +1,70 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+    public class TestSceneBlueprintSelection : EditorTestScene
+    {
+        protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+        private BlueprintContainer blueprintContainer
+            => Editor.ChildrenOfType<BlueprintContainer>().First();
+
+        [Test]
+        public void TestSelectedObjectHasPriorityWhenOverlapping()
+        {
+            var firstSlider = new Slider
+            {
+                Path = new SliderPath(new[]
+                {
+                    new PathControlPoint(new Vector2()),
+                    new PathControlPoint(new Vector2(150, -50)),
+                    new PathControlPoint(new Vector2(300, 0))
+                }),
+                Position = new Vector2(0, 100)
+            };
+            var secondSlider = new Slider
+            {
+                Path = new SliderPath(new[]
+                {
+                    new PathControlPoint(new Vector2()),
+                    new PathControlPoint(new Vector2(-50, 50)),
+                    new PathControlPoint(new Vector2(-100, 100))
+                }),
+                Position = new Vector2(200, 0)
+            };
+
+            AddStep("add overlapping sliders", () =>
+            {
+                EditorBeatmap.Add(firstSlider);
+                EditorBeatmap.Add(secondSlider);
+            });
+            AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
+
+            AddStep("move mouse to common point", () =>
+            {
+                var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
+                InputManager.MoveMouseTo(pos);
+            });
+            AddStep("right click", () => InputManager.Click(MouseButton.Right));
+
+            AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
+        }
+    }
+}

From dd48b68f8ad6fc513d17f9847d0863df2c90dc1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Fri, 19 Mar 2021 22:20:40 +0100
Subject: [PATCH 331/434] Ensure selected blueprints are given selection
 priority

---
 osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 051d0766bf..7def7e1d16 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -338,7 +338,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
         private bool beginClickSelection(MouseButtonEvent e)
         {
             // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
-            foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
+            // Priority is given to already-selected blueprints.
+            foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
             {
                 if (!blueprint.IsHovered) continue;
 

From ca943a897a387e7a7f7d8a934aee7125f262f698 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Sat, 20 Mar 2021 10:51:58 +0900
Subject: [PATCH 332/434] Fix back to front initialisation order

---
 osu.Game/Skinning/SkinnableSound.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 9c6a4f7970..f935adf7a5 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -133,10 +133,11 @@ namespace osu.Game.Skinning
 
         protected override void LoadAsyncComplete()
         {
-            base.LoadAsyncComplete();
-
+            // ensure samples are constructed before SkinChanged() is called via base.LoadAsyncComplete().
             if (!samplesContainer.Any())
                 updateSamples();
+
+            base.LoadAsyncComplete();
         }
 
         /// <summary>

From d28bed6ed29f9aeea901ccbec2c075367007fb47 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 20 Mar 2021 12:29:24 +0100
Subject: [PATCH 333/434] Schedule adding transforms on tablet changes

Fixes `InvalidThreadForMutationException`s that pop up when
disconnecting/reconnecting tablets during the game's operation. In those
cases the value change callback executes from  an OpenTabletDriver
thread.
---
 .../Sections/Input/TabletAreaSelection.cs     | 15 ++++++-----
 .../Settings/Sections/Input/TabletSettings.cs | 25 +++++++++++++------
 2 files changed, 26 insertions(+), 14 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
index df25668411..ecb8acce54 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -125,17 +125,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             }, true);
 
             tablet.BindTo(handler.Tablet);
-            tablet.BindValueChanged(val =>
-            {
-                tabletContainer.Size = val.NewValue?.Size ?? Vector2.Zero;
-                tabletName.Text = val.NewValue?.Name ?? string.Empty;
-                checkBounds();
-            }, true);
+            tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails));
 
+            updateTabletDetails();
             // initial animation should be instant.
             FinishTransforms(true);
         }
 
+        private void updateTabletDetails()
+        {
+            tabletContainer.Size = tablet.Value?.Size ?? Vector2.Zero;
+            tabletName.Text = tablet.Value?.Name ?? string.Empty;
+            checkBounds();
+        }
+
         private static int greatestCommonDivider(int a, int b)
         {
             while (b != 0)
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index b06b148984..4baf43783d 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -196,19 +196,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             tablet.BindTo(tabletHandler.Tablet);
             tablet.BindValueChanged(val =>
             {
+                Scheduler.AddOnce(toggleVisibility);
+
                 var tab = val.NewValue;
 
                 bool tabletFound = tab != null;
-
                 if (!tabletFound)
-                {
-                    mainSettings.Hide();
-                    noTabletMessage.Show();
                     return;
-                }
-
-                mainSettings.Show();
-                noTabletMessage.Hide();
 
                 offsetX.MaxValue = tab.Size.X;
                 offsetX.Default = tab.Size.X / 2;
@@ -222,6 +216,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             }, true);
         }
 
+        private void toggleVisibility()
+        {
+            bool tabletFound = tablet.Value != null;
+
+            if (!tabletFound)
+            {
+                mainSettings.Hide();
+                noTabletMessage.Show();
+                return;
+            }
+
+            mainSettings.Show();
+            noTabletMessage.Hide();
+        }
+
         private void applyAspectRatio(BindableNumber<float> sizeChanged)
         {
             try

From 86b569f5f7a13700b7ef728fafb6fe1c014ea8d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sat, 20 Mar 2021 12:34:41 +0100
Subject: [PATCH 334/434] Fix typo in identifier

---
 osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 4baf43783d..bd0f7ddc4c 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             {
                 if (!aspectLock.Value)
                 {
-                    float proposedAspectRatio = curentAspectRatio;
+                    float proposedAspectRatio = currentAspectRatio;
 
                     if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
                     {
@@ -278,8 +278,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
             aspectLock.Value = true;
         }
 
-        private void updateAspectRatio() => aspectRatio.Value = curentAspectRatio;
+        private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio;
 
-        private float curentAspectRatio => sizeX.Value / sizeY.Value;
+        private float currentAspectRatio => sizeX.Value / sizeY.Value;
     }
 }

From a16c0641b27ab69f59c56a4cf96f388e941affa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sun, 21 Mar 2021 11:01:06 +0100
Subject: [PATCH 335/434] Revert EF Core to version 2.2

This reverts commit f3faad74d587bbbd104395f5072723203c9d54aa, reversing
changes made to 712e7bc7bfaa94dd8c7248d9e800e7bc916476c0.

Several issues arose after migrating to 5.0, including, but possibly not
limited to, performance regressions in song select, as well as failures
when attempting to save beatmaps after metadata changes in the editor.
---
 osu.Desktop/osu.Desktop.csproj                |  4 +-
 .../osu.Game.Rulesets.Catch.Tests.csproj      |  2 +-
 .../osu.Game.Rulesets.Mania.Tests.csproj      |  2 +-
 .../osu.Game.Rulesets.Osu.Tests.csproj        |  2 +-
 .../osu.Game.Rulesets.Taiko.Tests.csproj      |  2 +-
 .../TestSceneMultiplayerMatchSongSelect.cs    |  1 -
 .../TestSceneBeatmapRecommendations.cs        |  1 -
 .../SongSelect/TestScenePlaySongSelect.cs     |  4 +-
 osu.Game.Tests/osu.Game.Tests.csproj          |  2 +-
 osu.Game/Beatmaps/BeatmapManager.cs           |  2 -
 osu.Game/Database/ArchiveModelManager.cs      |  6 +-
 .../Database/DatabaseWorkaroundExtensions.cs  | 71 -------------------
 osu.Game/Database/OsuDbContext.cs             |  9 +--
 osu.Game/Scoring/ScoreInfo.cs                 | 13 +++-
 osu.Game/Scoring/ScoreManager.cs              |  5 --
 osu.Game/Skinning/SkinManager.cs              |  5 --
 osu.Game/osu.Game.csproj                      |  6 +-
 osu.iOS.props                                 |  2 +
 18 files changed, 30 insertions(+), 109 deletions(-)
 delete mode 100644 osu.Game/Database/DatabaseWorkaroundExtensions.cs

diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index d9d23dea6b..3e0f0cb7f6 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,8 +27,8 @@
     <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
     <PackageReference Include="System.IO.Packaging" Version="5.0.0" />
     <PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
     <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
     <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
   </ItemGroup>
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 42f70151ac..728af5124e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index e51b20c9fe..af16f39563 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index f1f75148ef..3d2d1f3fec 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index c9a320bdd5..fa00922706 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -5,7 +5,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
   </ItemGroup>
   <PropertyGroup Label="Project">
     <OutputType>WinExe</OutputType>
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 8cfe5d8af2..faa5d9e6fc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -56,7 +56,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
                 beatmaps.Add(new BeatmapInfo
                 {
                     Ruleset = rulesets.GetRuleset(i % 4),
-                    RulesetID = i % 4, // workaround for efcore 5 compatibility.
                     OnlineBeatmapID = beatmapId,
                     Length = length,
                     BPM = bpm,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 9b8b74e6f6..53a956c77c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -186,7 +186,6 @@ namespace osu.Game.Tests.Visual.SongSelect
                     Metadata = metadata,
                     BaseDifficulty = new BeatmapDifficulty(),
                     Ruleset = ruleset,
-                    RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
                     StarDifficulty = difficultyIndex + 1,
                     Version = $"SR{difficultyIndex + 1}"
                 }).ToList()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 057b539e44..5731b1ac2c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -911,11 +911,9 @@ namespace osu.Game.Tests.Visual.SongSelect
                 int length = RNG.Next(30000, 200000);
                 double bpm = RNG.NextSingle(80, 200);
 
-                var ruleset = getRuleset();
                 beatmaps.Add(new BeatmapInfo
                 {
-                    Ruleset = ruleset,
-                    RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
+                    Ruleset = getRuleset(),
                     OnlineBeatmapID = beatmapId,
                     Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
                     Length = length,
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 6f8e0fac6f..e36b3cdc74 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -6,7 +6,7 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
-    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
+    <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
     <PackageReference Include="Moq" Version="4.16.1" />
   </ItemGroup>
   <PropertyGroup Label="Project">
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 115d1b33bb..b4ea898b7d 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -171,8 +171,6 @@ namespace osu.Game.Beatmaps
             if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
                 throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
 
-            beatmapSet.Requery(ContextFactory);
-
             // check if a set already exists with the same online id, delete if it does.
             if (beatmapSet.OnlineBeatmapSetID != null)
             {
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 64428882ac..d809dbcb01 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -462,8 +462,6 @@ namespace osu.Game.Database
                 // Dereference the existing file info, since the file model will be removed.
                 if (file.FileInfo != null)
                 {
-                    file.Requery(usage.Context);
-
                     Files.Dereference(file.FileInfo);
 
                     // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
@@ -637,12 +635,10 @@ namespace osu.Game.Database
             {
                 using (Stream s = reader.GetStream(file))
                 {
-                    var fileInfo = files.Add(s);
                     fileInfos.Add(new TFileModel
                     {
                         Filename = file.Substring(prefix.Length).ToStandardisedPath(),
-                        FileInfo = fileInfo,
-                        FileInfoID = fileInfo.ID // workaround for efcore 5 compatibility.
+                        FileInfo = files.Add(s)
                     });
                 }
             }
diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
deleted file mode 100644
index a3a982f232..0000000000
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using osu.Game.Beatmaps;
-using osu.Game.Scoring;
-using osu.Game.Skinning;
-
-namespace osu.Game.Database
-{
-    /// <summary>
-    /// Extension methods which contain workarounds to make EFcore 5.x work with our existing (incorrect) thread safety.
-    /// The intention is to avoid blocking package updates while we consider the future of the database backend, with a potential backend switch imminent.
-    /// </summary>
-    public static class DatabaseWorkaroundExtensions
-    {
-        /// <summary>
-        /// Re-query the provided model to ensure it is in a sane state. This method requires explicit implementation per model type.
-        /// </summary>
-        /// <param name="model"></param>
-        /// <param name="contextFactory"></param>
-        public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory)
-        {
-            switch (model)
-            {
-                case SkinInfo skinInfo:
-                    requeryFiles(skinInfo.Files, contextFactory);
-                    break;
-
-                case ScoreInfo scoreInfo:
-                    requeryFiles(scoreInfo.Beatmap.BeatmapSet.Files, contextFactory);
-                    requeryFiles(scoreInfo.Files, contextFactory);
-                    break;
-
-                case BeatmapSetInfo beatmapSetInfo:
-                    var context = contextFactory.Get();
-
-                    foreach (var beatmap in beatmapSetInfo.Beatmaps)
-                    {
-                        // Workaround System.InvalidOperationException
-                        // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-                        beatmap.Ruleset = context.RulesetInfo.Find(beatmap.RulesetID);
-                    }
-
-                    requeryFiles(beatmapSetInfo.Files, contextFactory);
-                    break;
-
-                default:
-                    throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model));
-            }
-
-            void requeryFiles<T>(List<T> files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
-            {
-                var dbContext = databaseContextFactory.Get();
-
-                foreach (var file in files)
-                {
-                    Requery(file, dbContext);
-                }
-            }
-        }
-
-        public static void Requery(this INamedFileInfo file, OsuDbContext dbContext)
-        {
-            // Workaround System.InvalidOperationException
-            // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
-            file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
-        }
-    }
-}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 2342ab07d4..2aae62edea 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -3,6 +3,7 @@
 
 using System;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
 using Microsoft.Extensions.Logging;
 using osu.Framework.Logging;
 using osu.Framework.Statistics;
@@ -110,10 +111,10 @@ namespace osu.Game.Database
         {
             base.OnConfiguring(optionsBuilder);
             optionsBuilder
-                .UseSqlite(connectionString,
-                    sqliteOptions => sqliteOptions
-                                     .CommandTimeout(10)
-                                     .UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
+                // this is required for the time being due to the way we are querying in places like BeatmapStore.
+                // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
+                .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
+                .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
                 .UseLoggerFactory(logger.Value);
         }
 
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 78101991f6..f5192f3a40 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Scoring
             }
             set
             {
-                modsJson = JsonConvert.SerializeObject(value.Select(m => new DeserializedMod { Acronym = m.Acronym }));
+                modsJson = null;
                 mods = value;
             }
         }
@@ -86,7 +86,16 @@ namespace osu.Game.Scoring
         [Column("Mods")]
         public string ModsJson
         {
-            get => modsJson;
+            get
+            {
+                if (modsJson != null)
+                    return modsJson;
+
+                if (mods == null)
+                    return null;
+
+                return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
+            }
             set
             {
                 modsJson = value;
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 7d0abc5996..c7ee26c248 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -53,11 +53,6 @@ namespace osu.Game.Scoring
             this.configManager = configManager;
         }
 
-        protected override void PreImport(ScoreInfo model)
-        {
-            model.Requery(ContextFactory);
-        }
-
         protected override ScoreInfo CreateModel(ArchiveReader archive)
         {
             if (archive == null)
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 894a068b7f..9257636301 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -142,11 +142,6 @@ namespace osu.Game.Skinning
             }
         }
 
-        protected override void PreImport(SkinInfo model)
-        {
-            model.Requery(ContextFactory);
-        }
-
         /// <summary>
         /// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
         /// </summary>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 360c522193..9731c1d5ea 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,10 +24,10 @@
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
     <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
-    <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
+    <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index b763a91dfb..11677d345e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -90,6 +90,8 @@
   <ItemGroup Label="Transitive Dependencies">
     <PackageReference Include="DiffPlex" Version="1.6.3" />
     <PackageReference Include="Humanizer" Version="2.8.26" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />

From c4f3714385c411dca8092f3163aad2817ff7f62b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sun, 21 Mar 2021 18:39:52 +0100
Subject: [PATCH 336/434] Make hold note input tests fail due to head hiding

---
 .../TestSceneHoldNoteInput.cs                        | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 596430f9e5..7ae69bf7d7 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -5,11 +5,13 @@ 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.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
 using osu.Game.Rulesets.Mania.Replays;
 using osu.Game.Rulesets.Mania.Scoring;
 using osu.Game.Rulesets.Objects;
@@ -345,6 +347,14 @@ namespace osu.Game.Rulesets.Mania.Tests
 
             AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
             AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+
+            AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head);
+            AddAssert("head is visible",
+                () => currentPlayer.ChildrenOfType<DrawableHoldNote>()
+                                   .Single(note => note.HitObject == beatmap.HitObjects[0])
+                                   .Head
+                                   .Alpha == 1);
+
             AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
         }
 
@@ -352,6 +362,8 @@ namespace osu.Game.Rulesets.Mania.Tests
         {
             public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
 
+            public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
+
             protected override bool PauseOnFocusLost => false;
 
             public ScoreAccessibleReplayPlayer(Score score)

From 9a330c4c56aeeda049cfccdd6a15f10c1966758c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Sun, 21 Mar 2021 18:34:31 +0100
Subject: [PATCH 337/434] Fix mania hold note heads hiding when frozen

This was an insidious regression from a3dc1d5. Prior to that commit,
`DrawableHoldNoteHead` had `UpdateStateTransforms()` overridden, to set
the hold note head's lifetime. When that method was split into
`UpdateInitialStateTransforms()` and `UpdateHitStateTransforms()`, the
lifetime set was moved to the former.

Unfortunately, that override served two purposes: both to set the
lifetime, and to suppress hit animations which would normally be added
by the base `DrawableManiaHitObject`. That fact being missed led to
`UpdateHitStateTransforms()` hiding the hold note head immediately on
hit and with a slight delay on miss.

To resolve, explicitly override `UpdateHitStateTransforms()` and
suppress the base call, with an explanatory comment.
---
 .../Objects/Drawables/DrawableHoldNoteHead.cs          | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 75dcf0e55e..35ba2465fa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,8 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using osu.Game.Rulesets.Objects.Drawables;
+
 namespace osu.Game.Rulesets.Mania.Objects.Drawables
 {
     /// <summary>
@@ -25,6 +27,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
             LifetimeEnd = LifetimeStart + 30000;
         }
 
+        protected override void UpdateHitStateTransforms(ArmedState state)
+        {
+            // suppress the base call explicitly.
+            // the hold note head should never change its visual state on its own due to the "freezing" mechanic
+            // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
+            // it will be hidden along with its parenting hold note when required.
+        }
+
         public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
 
         public override void OnReleased(ManiaAction action)

From e31d583a7f468853844bb1e8aa38f2d70225f122 Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Sun, 21 Mar 2021 11:16:59 -0700
Subject: [PATCH 338/434] Add comments count to user profile overlay

---
 osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 6 ++++++
 osu.Game/Users/User.cs                                    | 3 +++
 2 files changed, 9 insertions(+)

diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 662f55317b..e73579fad0 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Linq;
+using Humanizer;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions;
@@ -115,6 +116,11 @@ namespace osu.Game.Overlays.Profile.Header
             topLinkContainer.AddText("Contributed ");
             topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
 
+            addSpacer(topLinkContainer);
+
+            topLinkContainer.AddText("Posted ");
+            topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden);
+
             string websiteWithoutProtocol = user.Website;
 
             if (!string.IsNullOrEmpty(websiteWithoutProtocol))
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 6c45417db0..74ffb7c457 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -120,6 +120,9 @@ namespace osu.Game.Users
         [JsonProperty(@"post_count")]
         public int PostCount;
 
+        [JsonProperty(@"comments_count")]
+        public int CommentsCount;
+
         [JsonProperty(@"follower_count")]
         public int FollowerCount;
 

From 9bc6cdf042317265f7e37b3feaf58f10770d6fac Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Sun, 21 Mar 2021 11:19:07 -0700
Subject: [PATCH 339/434] Fix singular format regression on forum post text

---
 osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index e73579fad0..fe61e532e1 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Overlays.Profile.Header
             }
 
             topLinkContainer.AddText("Contributed ");
-            topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
+            topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
 
             addSpacer(topLinkContainer);
 

From f7bf23dbe9b51fa6f2a8168efdf4ce1b081e8874 Mon Sep 17 00:00:00 2001
From: owen-young <owen.young8699@gmail.com>
Date: Sun, 21 Mar 2021 21:50:19 -0500
Subject: [PATCH 340/434] first attempt at changing windowMode to be fullscreen
 on default

---
 osu.Game/OsuGame.cs | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index dd775888a1..2fd6331c86 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -126,6 +126,8 @@ namespace osu.Game
 
         private Bindable<int> configSkin;
 
+        private Bindable<WindowMode> windowMode;
+
         private readonly string[] args;
 
         private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
@@ -631,6 +633,12 @@ namespace osu.Game
 
             loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true);
 
+            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
+            windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
+            {
+                    windowMode.Value = WindowMode.Windowed;
+            }), true);
+
             var onScreenDisplay = new OnScreenDisplay();
 
             onScreenDisplay.BeginTracking(this, frameworkConfig);

From 073dba5330663073433b38798626ae0d6781d971 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Mar 2021 14:05:37 +0900
Subject: [PATCH 341/434] Remove local workarounds to attempt to avoid crashes
 on skin change

---
 osu.Game/Skinning/LegacySkin.cs | 76 +--------------------------------
 1 file changed, 1 insertion(+), 75 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index ec49d43c67..12abc4d867 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -7,7 +7,6 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using JetBrains.Annotations;
-using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -462,7 +461,7 @@ namespace osu.Game.Skinning
                 var sample = Samples?.Get(lookup);
 
                 if (sample != null)
-                    return new LegacySkinSample(sample, this);
+                    return sample;
             }
 
             return null;
@@ -505,78 +504,5 @@ namespace osu.Game.Skinning
             Textures?.Dispose();
             Samples?.Dispose();
         }
-
-        /// <summary>
-        /// A sample wrapper which keeps a reference to the contained skin to avoid finalizer garbage collection of the managing SampleStore.
-        /// </summary>
-        private class LegacySkinSample : ISample, IDisposable
-        {
-            private readonly Sample sample;
-
-            [UsedImplicitly]
-            private readonly LegacySkin skin;
-
-            public LegacySkinSample(Sample sample, LegacySkin skin)
-            {
-                this.sample = sample;
-                this.skin = skin;
-            }
-
-            public SampleChannel Play()
-            {
-                return sample.Play();
-            }
-
-            public SampleChannel GetChannel()
-            {
-                return sample.GetChannel();
-            }
-
-            public double Length => sample.Length;
-
-            public Bindable<int> PlaybackConcurrency => sample.PlaybackConcurrency;
-            public BindableNumber<double> Volume => sample.Volume;
-
-            public BindableNumber<double> Balance => sample.Balance;
-
-            public BindableNumber<double> Frequency => sample.Frequency;
-
-            public BindableNumber<double> Tempo => sample.Tempo;
-
-            public void BindAdjustments(IAggregateAudioAdjustment component)
-            {
-                sample.BindAdjustments(component);
-            }
-
-            public void UnbindAdjustments(IAggregateAudioAdjustment component)
-            {
-                sample.UnbindAdjustments(component);
-            }
-
-            public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
-            {
-                sample.AddAdjustment(type, adjustBindable);
-            }
-
-            public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
-            {
-                sample.RemoveAdjustment(type, adjustBindable);
-            }
-
-            public void RemoveAllAdjustments(AdjustableProperty type)
-            {
-                sample.RemoveAllAdjustments(type);
-            }
-
-            public IBindable<double> AggregateVolume => sample.AggregateVolume;
-
-            public IBindable<double> AggregateBalance => sample.AggregateBalance;
-
-            public IBindable<double> AggregateFrequency => sample.AggregateFrequency;
-
-            public IBindable<double> AggregateTempo => sample.AggregateTempo;
-
-            public void Dispose() => sample.Dispose();
-        }
     }
 }

From db64fac8241710308f02399909ec7f6f05be6251 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Mar 2021 15:26:22 +0900
Subject: [PATCH 342/434] Delay key fade in legacy mania skins

---
 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 78ccb83a8c..174324f5f6 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -101,8 +101,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
         {
             if (action == column.Action.Value)
             {
-                upSprite.FadeTo(1);
-                downSprite.FadeTo(0);
+                upSprite.Delay(80).FadeTo(1);
+                downSprite.Delay(80).FadeTo(0);
             }
         }
     }

From fc632fd48aae56b1045a1955b035d8ac574609d8 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Mon, 22 Mar 2021 01:30:20 -0500
Subject: [PATCH 343/434] Added WindowSetting setting to OsuSetting enum so
 that it can be set by default at startup. Modified LayoutSettings.cs so that
 when it is changed in the settings, it is written to the local settings as
 well.

---
 osu.Game/Configuration/OsuConfigManager.cs               | 3 +++
 osu.Game/OsuGame.cs                                      | 9 +++------
 .../Settings/Sections/Graphics/LayoutSettings.cs         | 7 ++++++-
 3 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index d0fa45bb7a..cd74fe25f4 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -91,6 +91,8 @@ namespace osu.Game.Configuration
 
             Set(OsuSetting.MenuParallax, true);
 
+            Set(OsuSetting.WindowSetting, WindowMode.Fullscreen);
+
             // Gameplay
             Set(OsuSetting.DimLevel, 0.8, 0, 1, 0.01);
             Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
@@ -233,6 +235,7 @@ namespace osu.Game.Configuration
         MenuVoice,
         CursorRotation,
         MenuParallax,
+        WindowSetting,
         BeatmapDetailTab,
         BeatmapDetailModsFilter,
         Username,
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 2fd6331c86..84737a56e4 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -234,6 +234,9 @@ namespace osu.Game
 
             SelectedMods.BindValueChanged(modsChanged);
             Beatmap.BindValueChanged(beatmapChanged, true);
+
+            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
+            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
         }
 
         private ExternalLinkOpener externalLinkOpener;
@@ -633,12 +636,6 @@ namespace osu.Game
 
             loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true);
 
-            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
-            windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
-            {
-                    windowMode.Value = WindowMode.Windowed;
-            }), true);
-
             var onScreenDisplay = new OnScreenDisplay();
 
             onScreenDisplay.BeginTracking(this, frameworkConfig);
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 4d5c2e06eb..ab662cb9a0 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
         private Bindable<ScalingMode> scalingMode;
         private Bindable<Size> sizeFullscreen;
+        private Bindable<WindowMode> windowMode;
 
         private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
 
@@ -56,6 +57,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
             scalingSizeY = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeY);
             scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
             scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
+            windowMode = osuConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
 
             if (host.Window != null)
             {
@@ -141,7 +143,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
             scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
 
-            windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
+            windowModeDropdown.Current.ValueChanged += mode => {
+                windowMode.Value = mode.NewValue;
+                updateResolutionDropdown();
+            };
 
             windowModes.BindCollectionChanged((sender, args) =>
             {

From c4d08463addcf532926308dc319eebb981d530a8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Mar 2021 16:04:51 +0900
Subject: [PATCH 344/434] Fix spinners playing looping sound too long in the
 editor

The `OnComplete` event was never being run due to the transform playing
out longer than the spinner's lifetime. I've matched the durations, but
also moved the `Stop()` call to what I deem a safer place to run it (I
did notice that without this it would still potentially never fire).

Note that this is more noticeable in the editor because of lifetime
extension. In gameplay, the returning of a spinner to the pool will
clean things up (but in the editor that can take longer, depending on
timeline zoom level).

Another thing worth mentioning is that the fade doesn't actually work.
This is due to https://github.com/ppy/osu-framework/pull/4212.

Closes #12119.
---
 .../Objects/Drawables/DrawableSpinner.cs         | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 3d614c2dbd..d92f63eb89 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
         private Bindable<bool> isSpinning;
         private bool spinnerFrequencyModulate;
 
+        private const double fade_out_duration = 160;
+
         public DrawableSpinner()
             : this(null)
         {
@@ -136,7 +138,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             }
             else
             {
-                spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop());
+                if (spinningSample != null)
+                    spinningSample.Volume.Value = 0;
+
+                spinningSample?.VolumeTo(0, fade_out_duration);
             }
         }
 
@@ -173,7 +178,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
         {
             base.UpdateHitStateTransforms(state);
 
-            this.FadeOut(160).Expire();
+            this.FadeOut(fade_out_duration).OnComplete(_ =>
+            {
+                // looping sample should be stopped here as it is safer than running in the OnComplete
+                // of the volume transition above.
+                spinningSample.Stop();
+            });
+
+            Expire();
 
             // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
             isSpinning?.TriggerChange();

From 690fb9224aaee2d2d4930de181992eb53970b32d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Mar 2021 16:18:31 +0900
Subject: [PATCH 345/434] Combine constants for readability

---
 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs | 4 +++-
 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs      | 4 ++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 73aece1ed4..9ec122a12c 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
 {
     public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
     {
+        public const double FADE_IN_DURATION = 80;
+
         private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
 
         private Drawable explosion;
@@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
 
             (explosion as IFramedAnimation)?.GotoFrame(0);
 
-            explosion?.FadeInFromZero(80)
+            explosion?.FadeInFromZero(fade_in_duration)
                      .Then().FadeOut(120);
         }
     }
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 174324f5f6..10319a7d4d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -101,8 +101,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
         {
             if (action == column.Action.Value)
             {
-                upSprite.Delay(80).FadeTo(1);
-                downSprite.Delay(80).FadeTo(0);
+                upSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(1);
+                downSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(0);
             }
         }
     }

From 5b1d9f4cf07abd14860bc9ecf887fa98e55b4dc6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 22 Mar 2021 16:19:29 +0900
Subject: [PATCH 346/434] Fix constant case

---
 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 9ec122a12c..e4d466dca5 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
 
             (explosion as IFramedAnimation)?.GotoFrame(0);
 
-            explosion?.FadeInFromZero(fade_in_duration)
+            explosion?.FadeInFromZero(FADE_IN_DURATION)
                      .Then().FadeOut(120);
         }
     }

From e60ff45b73a2e236eed250f869a3b4afbafd3292 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Mon, 22 Mar 2021 16:57:40 +0900
Subject: [PATCH 347/434] Add another test for colinear perfect curves

---
 .../colinear-perfect-curve-expected-conversion.json | 13 +++++++++++++
 .../Testing/Beatmaps/colinear-perfect-curve.osu     |  4 +++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
index 96e4bf1637..1a0bd66246 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
@@ -1,5 +1,18 @@
 {
     "Mappings": [{
+        "StartTime": 114993,
+        "Objects": [{
+            "StartTime": 114993,
+            "EndTime": 114993,
+            "X": 493,
+            "Y": 92
+        }, {
+            "StartTime": 115290,
+            "EndTime": 115290,
+            "X": 451.659241,
+            "Y": 267.188
+        }]
+    }, {
         "StartTime": 118858.0,
         "Objects": [{
             "StartTime": 118858.0,
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
index 8c3edc9571..dd35098502 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
@@ -9,7 +9,9 @@ SliderMultiplier:1.87
 SliderTickRate:1
 
 [TimingPoints]
-49051,230.769230769231,4,2,1,15,1,0
+114000,346.820809248555,4,2,1,71,1,0
+118000,230.769230769231,4,2,1,15,1,0
 
 [HitObjects]
+493,92,114993,2,0,P|472:181|442:308,1,180,12|0,0:0|0:0,0:0:0:0:
 219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0:

From a65e491768441017e9cfe7e2e4fec2d15e57d708 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 22 Mar 2021 20:00:36 +0100
Subject: [PATCH 348/434] Remove osuTK desktop rider run config

No longer operational since 6eadae8.
---
 .../runConfigurations/osu___legacy_osuTK_.xml | 20 -------------------
 1 file changed, 20 deletions(-)
 delete mode 100644 .idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml

diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
deleted file mode 100644
index 9ece926b34..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="osu! (legacy osuTK)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
-    <option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0/osu!.dll" />
-    <option name="PROGRAM_PARAMETERS" value="--tk" />
-    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0" />
-    <option name="PASS_PARENT_ENVS" value="1" />
-    <option name="USE_EXTERNAL_CONSOLE" value="0" />
-    <option name="USE_MONO" value="0" />
-    <option name="RUNTIME_ARGUMENTS" value="" />
-    <option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Desktop/osu.Desktop.csproj" />
-    <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
-    <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
-    <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
-    <option name="PROJECT_KIND" value="DotNetCore" />
-    <option name="PROJECT_TFM" value="net5.0" />
-    <method v="2">
-      <option name="Build" />
-    </method>
-  </configuration>
-</component>
\ No newline at end of file

From d85929d721254391d5e7e23d2e81d56180ee07f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 22 Mar 2021 22:45:18 +0100
Subject: [PATCH 349/434] Adjust autoplay generation tests to match expected
 behaviour

---
 .../TestSceneAutoGeneration.cs                         | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index a5248c7712..399a46aa77 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests
 
             Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
             Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
-            Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+            Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
             Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
             Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
         }
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
             Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
 
             Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
-            Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+            Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
 
             Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
             Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
@@ -148,9 +148,9 @@ namespace osu.Game.Rulesets.Mania.Tests
 
             Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
             Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
-            Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
+            Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
             Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
-            Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
+            Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
             Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
             Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
             Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
@@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Tests
             // |   |   |
 
             var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
-            beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY });
+            beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
             beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
 
             var generated = new ManiaAutoGenerator(beatmap).Generate();

From 29d4162e4e2110973571a8fcfe83509e3586d50a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= <dach.bartlomiej@gmail.com>
Date: Mon, 22 Mar 2021 22:38:51 +0100
Subject: [PATCH 350/434] Remove release delay for hold notes when generating
 autoplay

It was more intended for normal notes anyway (as they would be released
pretty much instantaneously, if it weren't for the delay).
---
 .../Replays/ManiaAutoGenerator.cs             | 25 +++++++++++++------
 1 file changed, 17 insertions(+), 8 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 3ebbe5af8e..7c51d58b74 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Game.Replays;
 using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Replays;
 
@@ -85,20 +86,28 @@ namespace osu.Game.Rulesets.Mania.Replays
             {
                 var currentObject = Beatmap.HitObjects[i];
                 var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
-
-                double endTime = currentObject.GetEndTime();
-
-                bool canDelayKeyUp = nextObjectInColumn == null ||
-                                     nextObjectInColumn.StartTime > endTime + RELEASE_DELAY;
-
-                double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9;
+                var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
 
                 yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
 
-                yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column };
+                yield return new ReleasePoint { Time = releaseTime, Column = currentObject.Column };
             }
         }
 
+        private double calculateReleaseTime(HitObject currentObject, HitObject nextObject)
+        {
+            double endTime = currentObject.GetEndTime();
+
+            if (currentObject is HoldNote)
+                // hold note releases must be timed exactly.
+                return endTime;
+
+            bool canDelayKeyUpFully = nextObject == null ||
+                                      nextObject.StartTime > endTime + RELEASE_DELAY;
+
+            return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9);
+        }
+
         protected override HitObject GetNextObject(int currentIndex)
         {
             int desiredColumn = Beatmap.HitObjects[currentIndex].Column;

From 8ea7271d5c96b062bebcb8b702da739d9ef4dac1 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Mon, 22 Mar 2021 19:48:52 -0500
Subject: [PATCH 351/434] moved windowmode code to LoadComplete (?)

---
 osu.Game/OsuGame.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 84737a56e4..ff215b63e5 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -234,9 +234,6 @@ namespace osu.Game
 
             SelectedMods.BindValueChanged(modsChanged);
             Beatmap.BindValueChanged(beatmapChanged, true);
-
-            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
-            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
         }
 
         private ExternalLinkOpener externalLinkOpener;
@@ -576,6 +573,9 @@ namespace osu.Game
 
             dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
 
+            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
+            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
+
             AddRange(new Drawable[]
             {
                 new VolumeControlReceptor

From bdcb9451f79798197825106a0762a0c823b771c7 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Mon, 22 Mar 2021 20:17:05 -0500
Subject: [PATCH 352/434] added code to OsuGameBase to default to fullscreen,
 but that might not be a good place to put it.

---
 osu.Game/OsuGameBase.cs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index e1c7b67a8c..bcd384604f 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -21,6 +21,7 @@ using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Online.API;
+using osu.Framework.Configuration;
 using osu.Framework.Graphics.Performance;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input;
@@ -119,6 +120,7 @@ namespace osu.Game
         protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
 
         private Bindable<bool> fpsDisplayVisible;
+        private Bindable<WindowMode> windowMode;
 
         public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
 
@@ -361,6 +363,9 @@ namespace osu.Game
             fpsDisplayVisible.ValueChanged += visible => { FrameStatistics.Value = visible.NewValue ? FrameStatisticsMode.Minimal : FrameStatisticsMode.None; };
             fpsDisplayVisible.TriggerChange();
 
+            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
+            windowMode.Value = WindowMode.Fullscreen;
+
             FrameStatistics.ValueChanged += e => fpsDisplayVisible.Value = e.NewValue != FrameStatisticsMode.None;
         }
 

From 098005393e999f5623769adc9e5458bb86e37513 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 10:38:37 +0900
Subject: [PATCH 353/434] Remove unnecessary null checks and debug code

---
 .../Objects/Drawables/DrawableSpinner.cs               | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index d92f63eb89..32a0a14dc0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -133,15 +133,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             if (tracking.NewValue)
             {
                 if (!spinningSample.IsPlaying)
-                    spinningSample?.Play();
-                spinningSample?.VolumeTo(1, 300);
+                    spinningSample.Play();
+
+                spinningSample.VolumeTo(1, 300);
             }
             else
             {
-                if (spinningSample != null)
-                    spinningSample.Volume.Value = 0;
-
-                spinningSample?.VolumeTo(0, fade_out_duration);
+                spinningSample.VolumeTo(0, fade_out_duration);
             }
         }
 

From 16b3f22caf2754d97aacbf40f0d429e8b3cecdce Mon Sep 17 00:00:00 2001
From: Joehu <madamba.joehu@outlook.com>
Date: Mon, 22 Mar 2021 19:32:17 -0700
Subject: [PATCH 354/434] Fix incorrect trash icon being used on deleted
 comments counter

---
 osu.Game/Overlays/Comments/DeletedCommentsCounter.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
index 56588ef0a8..8c40d79f7a 100644
--- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
+++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments
                 {
                     new SpriteIcon
                     {
-                        Icon = FontAwesome.Solid.Trash,
+                        Icon = FontAwesome.Regular.TrashAlt,
                         Size = new Vector2(14),
                     },
                     countText = new OsuSpriteText

From 9f788f58548d629144c4b8946b74542b424ea7a4 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Mon, 22 Mar 2021 22:52:16 -0500
Subject: [PATCH 355/434] removed code from OsuGameBase for
 fullscreen.....OsuSetting still exists but cannot figure out a way to set it
 to a default and have it actually work

---
 osu.Game/OsuGame.cs                                         | 6 +++---
 osu.Game/OsuGameBase.cs                                     | 4 ----
 .../Overlays/Settings/Sections/Graphics/LayoutSettings.cs   | 6 ++++--
 3 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index ff215b63e5..2f2428e781 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -126,7 +126,7 @@ namespace osu.Game
 
         private Bindable<int> configSkin;
 
-        private Bindable<WindowMode> windowMode;
+        // private Bindable<WindowMode> windowMode;
 
         private readonly string[] args;
 
@@ -573,8 +573,8 @@ namespace osu.Game
 
             dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
 
-            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
-            frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
+            // windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
+            // frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
 
             AddRange(new Drawable[]
             {
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index bcd384604f..8b1fe20708 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -120,7 +120,6 @@ namespace osu.Game
         protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
 
         private Bindable<bool> fpsDisplayVisible;
-        private Bindable<WindowMode> windowMode;
 
         public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
 
@@ -363,9 +362,6 @@ namespace osu.Game
             fpsDisplayVisible.ValueChanged += visible => { FrameStatistics.Value = visible.NewValue ? FrameStatisticsMode.Minimal : FrameStatisticsMode.None; };
             fpsDisplayVisible.TriggerChange();
 
-            windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
-            windowMode.Value = WindowMode.Fullscreen;
-
             FrameStatistics.ValueChanged += e => fpsDisplayVisible.Value = e.NewValue != FrameStatisticsMode.None;
         }
 
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index ab662cb9a0..a0cb8fc2de 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Localisation;
+using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.Configuration;
 using osu.Game.Graphics.Containers;
@@ -57,7 +58,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
             scalingSizeY = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeY);
             scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
             scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
-            windowMode = osuConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
+            windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
+            Logger.Log($"windowMode {windowMode.Value}");
 
             if (host.Window != null)
             {
@@ -71,7 +73,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
                 {
                     LabelText = "Screen mode",
                     ItemSource = windowModes,
-                    Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
+                    Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode)
                 },
                 resolutionDropdown = new ResolutionSettingsDropdown
                 {

From d9e2c44a34c17d9ed0f93587460fd506480ad478 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Mon, 22 Mar 2021 23:36:55 -0500
Subject: [PATCH 356/434] implemented GetFrameworkConfigDefaults for overriding
 framework default, removed previous code that added a new OsuSetting and
 modified settings layout.

---
 osu.Game/Configuration/OsuConfigManager.cs            |  3 ---
 osu.Game/OsuGame.cs                                   | 11 ++++++-----
 osu.Game/OsuGameBase.cs                               |  1 -
 .../Settings/Sections/Graphics/LayoutSettings.cs      | 11 ++---------
 4 files changed, 8 insertions(+), 18 deletions(-)

diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index cd74fe25f4..d0fa45bb7a 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -91,8 +91,6 @@ namespace osu.Game.Configuration
 
             Set(OsuSetting.MenuParallax, true);
 
-            Set(OsuSetting.WindowSetting, WindowMode.Fullscreen);
-
             // Gameplay
             Set(OsuSetting.DimLevel, 0.8, 0, 1, 0.01);
             Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
@@ -235,7 +233,6 @@ namespace osu.Game.Configuration
         MenuVoice,
         CursorRotation,
         MenuParallax,
-        WindowSetting,
         BeatmapDetailTab,
         BeatmapDetailModsFilter,
         Username,
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 2f2428e781..ffb694c27e 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -126,8 +126,6 @@ namespace osu.Game
 
         private Bindable<int> configSkin;
 
-        // private Bindable<WindowMode> windowMode;
-
         private readonly string[] args;
 
         private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
@@ -573,9 +571,6 @@ namespace osu.Game
 
             dependencies.CacheAs(idleTracker = new GameIdleTracker(6000));
 
-            // windowMode = LocalConfig.GetBindable<WindowMode>(OsuSetting.WindowSetting);
-            // frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode).Value = windowMode.Value;
-
             AddRange(new Drawable[]
             {
                 new VolumeControlReceptor
@@ -1012,5 +1007,11 @@ namespace osu.Game
             if (newScreen == null)
                 Exit();
         }
+
+        protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() {
+            IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
+            defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);
+            return defaultOverrides;
+        }
     }
 }
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 8b1fe20708..e1c7b67a8c 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -21,7 +21,6 @@ using osu.Game.Configuration;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Online.API;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics.Performance;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input;
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index a0cb8fc2de..4d5c2e06eb 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -12,7 +12,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Localisation;
-using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.Configuration;
 using osu.Game.Graphics.Containers;
@@ -32,7 +31,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
         private Bindable<ScalingMode> scalingMode;
         private Bindable<Size> sizeFullscreen;
-        private Bindable<WindowMode> windowMode;
 
         private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
 
@@ -58,8 +56,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
             scalingSizeY = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeY);
             scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
             scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
-            windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
-            Logger.Log($"windowMode {windowMode.Value}");
 
             if (host.Window != null)
             {
@@ -73,7 +69,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
                 {
                     LabelText = "Screen mode",
                     ItemSource = windowModes,
-                    Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode)
+                    Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
                 },
                 resolutionDropdown = new ResolutionSettingsDropdown
                 {
@@ -145,10 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
             scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
 
-            windowModeDropdown.Current.ValueChanged += mode => {
-                windowMode.Value = mode.NewValue;
-                updateResolutionDropdown();
-            };
+            windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
 
             windowModes.BindCollectionChanged((sender, args) =>
             {

From 58c60100b431e6ce6ee720ff72206e9f9071d070 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 14:04:47 +0900
Subject: [PATCH 357/434] Fix APIScoreToken's data type not matching server
 side

---
 osu.Game/Online/Rooms/APIScoreToken.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Online/Rooms/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs
index f652c1720d..6b559876de 100644
--- a/osu.Game/Online/Rooms/APIScoreToken.cs
+++ b/osu.Game/Online/Rooms/APIScoreToken.cs
@@ -8,6 +8,6 @@ namespace osu.Game.Online.Rooms
     public class APIScoreToken
     {
         [JsonProperty("id")]
-        public int ID { get; set; }
+        public long ID { get; set; }
     }
 }

From 9c690f9545959c117b4358e391333b3882abb34c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 14:08:00 +0900
Subject: [PATCH 358/434] Fix second usage

---
 osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index a75e4bdc07..2b6dbd9dcb 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
 
         protected readonly PlaylistItem PlaylistItem;
 
-        protected int? Token { get; private set; }
+        protected long? Token { get; private set; }
 
         [Resolved]
         private IAPIProvider api { get; set; }

From 254b0f5dc3b2469fdccf6baf0231af30baf11c9b Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Tue, 23 Mar 2021 00:24:33 -0500
Subject: [PATCH 359/434] removed line (?) - tried doing testing to see if it
 launched in fullscreen (i.e., overriding the method ppy mentioned), but to no
 avail :(

---
 osu.Game/OsuGame.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index ffb694c27e..a52899433a 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1007,7 +1007,6 @@ namespace osu.Game
             if (newScreen == null)
                 Exit();
         }
-
         protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() {
             IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
             defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);

From 1171214541361b72ae23405a3549fd918af16fed Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 14:51:22 +0900
Subject: [PATCH 360/434] Update framework

---
 osu.Android.props        | 2 +-
 osu.Game/osu.Game.csproj | 2 +-
 osu.iOS.props            | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index e0392bd687..75ac298626 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.317.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.323.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9731c1d5ea..b90c938a8b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.323.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index 11677d345e..ce182a3054 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.317.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.323.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -93,7 +93,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.317.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.323.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From 08fcdc8ee46748eee4c8d66d5842b57f16d9d896 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 15:38:00 +0900
Subject: [PATCH 361/434] Update difficulty calculator tests with floating
 point differences

---
 .../OsuDifficultyCalculatorTest.cs                        | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index a365ea10d4..c2119585ab 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
     {
         protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
 
-        [TestCase(6.9311451172608853d, "diffcalc-test")]
-        [TestCase(1.0736587013228804d, "zero-length-sliders")]
+        [TestCase(6.9311451172574934d, "diffcalc-test")]
+        [TestCase(1.0736586907780401d, "zero-length-sliders")]
         public void Test(double expected, string name)
             => base.Test(expected, name);
 
-        [TestCase(8.6228371119393064d, "diffcalc-test")]
-        [TestCase(1.2864585434597433d, "zero-length-sliders")]
+        [TestCase(8.6228371119271454d, "diffcalc-test")]
+        [TestCase(1.2864585280364178d, "zero-length-sliders")]
         public void TestClockRateAdjusted(double expected, string name)
             => Test(expected, name, new OsuModDoubleTime());
 

From f5ba746ae5522e6c1ba34f6a34218491ff4aa626 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 17:31:28 +0900
Subject: [PATCH 362/434] Fail all API requests sent to DummyAPIAccess

Until now, API requests sent to dummy API were just lost in the void. In most cases this somehow worked as expected, but any logic which is waiting on a request to finish will potentially never get a response.

Going forward, I'm not 100% sure that every `Wait` on a web response will have local timeout logic (I think there is a certain amount of assumption that this is being managed for us by `APIAccess`), so I've made this change to better handle such cases going forward. Now, rather than nothing happening, requests will trigger a failure via the existing exception logic rather than silently pretending the request never arrived.
---
 osu.Game/Online/API/APIRequest.cs     | 8 ++++++--
 osu.Game/Online/API/DummyAPIAccess.cs | 7 ++++++-
 2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index a7174324d8..16d1b3ab17 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -181,9 +181,13 @@ namespace osu.Game.Online.API
         /// <returns>Whether we are in a failed or cancelled state.</returns>
         private bool checkAndScheduleFailure()
         {
-            if (API == null || pendingFailure == null) return cancelled;
+            if (pendingFailure == null) return cancelled;
+
+            if (API == null)
+                pendingFailure();
+            else
+                API.Schedule(pendingFailure);
 
-            API.Schedule(pendingFailure);
             pendingFailure = null;
             return true;
         }
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 943b52db88..3cb22381c1 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -55,7 +55,12 @@ namespace osu.Game.Online.API
 
         public virtual void Queue(APIRequest request)
         {
-            HandleRequest?.Invoke(request);
+            if (HandleRequest != null)
+                HandleRequest.Invoke(request);
+            else
+                // this will fail due to not receiving an APIAccess, and trigger a failure on the request.
+                // this is intended - any request in testing that needs non-failures should use HandleRequest.
+                request.Perform(this);
         }
 
         public void Perform(APIRequest request) => HandleRequest?.Invoke(request);

From ce452565f45bba3106ffe89a21c63d4662e8a40e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 17:50:31 +0900
Subject: [PATCH 363/434] Avoid firing any kind of failures after success

---
 osu.Game/Online/API/APIRequest.cs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 16d1b3ab17..1a6868cfa4 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -131,8 +131,11 @@ namespace osu.Game.Online.API
         {
         }
 
+        private bool succeeded;
+
         internal virtual void TriggerSuccess()
         {
+            succeeded = true;
             Success?.Invoke();
         }
 
@@ -145,10 +148,7 @@ namespace osu.Game.Online.API
 
         public void Fail(Exception e)
         {
-            if (WebRequest?.Completed == true)
-                return;
-
-            if (cancelled)
+            if (succeeded || cancelled)
                 return;
 
             cancelled = true;

From aeff9bd8531962e7d509623407a819f3c7f69ae8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 18:08:32 +0900
Subject: [PATCH 364/434] Add return bool to HandleRequest to better trigger
 failures

---
 .../Online/TestDummyAPIRequestHandling.cs     |  8 ++++++--
 .../TestSceneSeasonalBackgroundLoader.cs      |  4 +++-
 .../Online/TestSceneBeatmapListingOverlay.cs  | 13 ++++++------
 .../Online/TestSceneChangelogOverlay.cs       |  6 ++++--
 .../Visual/Online/TestSceneChatOverlay.cs     | 20 +++++++++++++++++++
 .../Online/TestSceneCommentsContainer.cs      |  3 ++-
 .../Visual/Online/TestSceneNewsOverlay.cs     |  3 ++-
 .../TestScenePlaylistsResultsScreen.cs        | 13 ++++++++++++
 .../TestSceneBeatmapRecommendations.cs        |  4 +++-
 osu.Game/Online/API/DummyAPIAccess.cs         |  9 +++++----
 .../Multiplayer/TestMultiplayerRoomManager.cs | 18 +++++++++--------
 11 files changed, 75 insertions(+), 26 deletions(-)

diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
index 42948c3731..aa29d76843 100644
--- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -23,8 +23,10 @@ namespace osu.Game.Tests.Online
                 {
                     case CommentVoteRequest cRequest:
                         cRequest.TriggerSuccess(new CommentBundle());
-                        break;
+                        return true;
                 }
+
+                return false;
             });
 
             CommentVoteRequest request = null;
@@ -108,8 +110,10 @@ namespace osu.Game.Tests.Online
                 {
                     case LeaveChannelRequest cRequest:
                         cRequest.TriggerSuccess();
-                        break;
+                        return true;
                 }
+
+                return false;
             });
         }
     }
diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
index e7cf830db0..dc5a4f4a3e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
@@ -135,13 +135,15 @@ namespace osu.Game.Tests.Visual.Background
                 dummyAPI.HandleRequest = request =>
                 {
                     if (dummyAPI.State.Value != APIState.Online || !(request is GetSeasonalBackgroundsRequest backgroundsRequest))
-                        return;
+                        return false;
 
                     backgroundsRequest.TriggerSuccess(new APISeasonalBackgrounds
                     {
                         Backgrounds = seasonal_background_urls.Select(url => new APISeasonalBackground { Url = url }).ToList(),
                         EndDate = endDate
                     });
+
+                    return true;
                 };
             });
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 1349264bf9..156d6b744e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -30,13 +30,14 @@ namespace osu.Game.Tests.Visual.Online
 
             ((DummyAPIAccess)API).HandleRequest = req =>
             {
-                if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)
+                if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
+
+                searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
                 {
-                    searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
-                    {
-                        BeatmapSets = setsForResponse,
-                    });
-                }
+                    BeatmapSets = setsForResponse,
+                });
+
+                return true;
             };
         }
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
index cd2c4e9346..8818ac75b1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
@@ -63,13 +63,15 @@ namespace osu.Game.Tests.Visual.Online
                             Builds = builds.Values.ToList()
                         };
                         changelogRequest.TriggerSuccess(changelogResponse);
-                        break;
+                        return true;
 
                     case GetChangelogBuildRequest buildRequest:
                         if (requestedBuild != null)
                             buildRequest.TriggerSuccess(requestedBuild);
-                        break;
+                        return true;
                 }
+
+                return false;
             };
 
             Child = changelog = new TestChangelogOverlay();
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index fca642ad6c..b13dd34ebc 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Testing;
 using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
 using osu.Game.Online.Chat;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Chat.Selection;
@@ -64,6 +66,24 @@ namespace osu.Game.Tests.Visual.Online
             });
         }
 
+        [SetUpSteps]
+        public void SetUpSteps()
+        {
+            AddStep("register request handling", () =>
+            {
+                ((DummyAPIAccess)API).HandleRequest = req =>
+                {
+                    switch (req)
+                    {
+                        case JoinChannelRequest _:
+                            return true;
+                    }
+
+                    return false;
+                };
+            });
+        }
+
         [Test]
         public void TestHideOverlay()
         {
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
index c2a18330c9..cd22bb2513 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
@@ -85,9 +85,10 @@ namespace osu.Game.Tests.Visual.Online
                 dummyAPI.HandleRequest = request =>
                 {
                     if (!(request is GetCommentsRequest getCommentsRequest))
-                        return;
+                        return false;
 
                     getCommentsRequest.TriggerSuccess(commentBundle);
+                    return true;
                 };
             });
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
index 37d51c16d2..6ebe8fcc07 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
@@ -33,9 +33,10 @@ namespace osu.Game.Tests.Visual.Online
                 dummyAPI.HandleRequest = request =>
                 {
                     if (!(request is GetNewsRequest getNewsRequest))
-                        return;
+                        return false;
 
                     getNewsRequest.TriggerSuccess(r);
+                    return true;
                 };
             });
 
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index be8032cde8..61d49e4018 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -170,6 +170,17 @@ namespace osu.Game.Tests.Visual.Playlists
 
         private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
         {
+            // pre-check for requests we should be handling (as they are scheduled below).
+            switch (request)
+            {
+                case ShowPlaylistUserScoreRequest _:
+                case IndexPlaylistScoresRequest _:
+                    break;
+
+                default:
+                    return false;
+            }
+
             requestComplete = false;
 
             double delay = delayed ? 3000 : 0;
@@ -196,6 +207,8 @@ namespace osu.Game.Tests.Visual.Playlists
                         break;
                 }
             }, delay);
+
+            return true;
         };
 
         private void triggerSuccess<T>(APIRequest<T> req, T result)
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 53a956c77c..5e2d5eba5d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -32,8 +32,10 @@ namespace osu.Game.Tests.Visual.SongSelect
                     {
                         case GetUserRequest userRequest:
                             userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID));
-                            break;
+                            return true;
                     }
+
+                    return false;
                 };
             });
 
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 3cb22381c1..52f2365165 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -34,8 +34,9 @@ namespace osu.Game.Online.API
 
         /// <summary>
         /// Provide handling logic for an arbitrary API request.
+        /// Should return true is a request was handled. If null or false return, the request will be failed with a <see cref="NotSupportedException"/>.
         /// </summary>
-        public Action<APIRequest> HandleRequest;
+        public Func<APIRequest, bool> HandleRequest;
 
         private readonly Bindable<APIState> state = new Bindable<APIState>(APIState.Online);
 
@@ -55,12 +56,12 @@ namespace osu.Game.Online.API
 
         public virtual void Queue(APIRequest request)
         {
-            if (HandleRequest != null)
-                HandleRequest.Invoke(request);
-            else
+            if (HandleRequest?.Invoke(request) != true)
+            {
                 // this will fail due to not receiving an APIAccess, and trigger a failure on the request.
                 // this is intended - any request in testing that needs non-failures should use HandleRequest.
                 request.Perform(this);
+            }
         }
 
         public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index 7e824c4d7c..315be510a3 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -52,15 +52,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                         Rooms.Add(createdRoom);
                         createRoomRequest.TriggerSuccess(createdRoom);
-                        break;
+                        return true;
 
                     case JoinRoomRequest joinRoomRequest:
                         joinRoomRequest.TriggerSuccess();
-                        break;
+                        return true;
 
                     case PartRoomRequest partRoomRequest:
                         partRoomRequest.TriggerSuccess();
-                        break;
+                        return true;
 
                     case GetRoomsRequest getRoomsRequest:
                         var roomsWithoutParticipants = new List<Room>();
@@ -76,11 +76,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
                         }
 
                         getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
-                        break;
+                        return true;
 
                     case GetRoomRequest getRoomRequest:
                         getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
-                        break;
+                        return true;
 
                     case GetBeatmapSetRequest getBeatmapSetRequest:
                         var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
@@ -89,11 +89,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
                         // Get the online API from the game's dependencies.
                         game.Dependencies.Get<IAPIProvider>().Queue(onlineReq);
-                        break;
+                        return true;
 
                     case CreateRoomScoreRequest createRoomScoreRequest:
                         createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
-                        break;
+                        return true;
 
                     case SubmitRoomScoreRequest submitRoomScoreRequest:
                         submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
@@ -108,8 +108,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
                             User = api.LocalUser.Value,
                             Statistics = new Dictionary<HitResult, int>()
                         });
-                        break;
+                        return true;
                 }
+
+                return false;
             };
         }
 

From 5267fb74c441c1c7f6ba48637f6981876ae5227e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 14:44:06 +0900
Subject: [PATCH 365/434] Add submission requests

---
 .../Online/Solo/CreateSoloScoreRequest.cs     | 32 +++++++++++++
 .../Online/Solo/SubmitSoloScoreRequest.cs     | 45 +++++++++++++++++++
 2 files changed, 77 insertions(+)
 create mode 100644 osu.Game/Online/Solo/CreateSoloScoreRequest.cs
 create mode 100644 osu.Game/Online/Solo/SubmitSoloScoreRequest.cs

diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
new file mode 100644
index 0000000000..ae5ac5e26c
--- /dev/null
+++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Online.Solo
+{
+    public class CreateSoloScoreRequest : APIRequest<APIScoreToken>
+    {
+        private readonly int beatmapId;
+        private readonly string versionHash;
+
+        public CreateSoloScoreRequest(int beatmapId, string versionHash)
+        {
+            this.beatmapId = beatmapId;
+            this.versionHash = versionHash;
+        }
+
+        protected override WebRequest CreateWebRequest()
+        {
+            var req = base.CreateWebRequest();
+            req.Method = HttpMethod.Post;
+            req.AddParameter("version_hash", versionHash);
+            return req;
+        }
+
+        protected override string Target => $@"solo/{beatmapId}/scores";
+    }
+}
diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
new file mode 100644
index 0000000000..98ba4fa052
--- /dev/null
+++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using Newtonsoft.Json;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.Solo
+{
+    public class SubmitSoloScoreRequest : APIRequest<MultiplayerScore>
+    {
+        private readonly long scoreId;
+
+        private readonly int beatmapId;
+
+        private readonly ScoreInfo scoreInfo;
+
+        public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo)
+        {
+            this.beatmapId = beatmapId;
+            this.scoreId = scoreId;
+            this.scoreInfo = scoreInfo;
+        }
+
+        protected override WebRequest CreateWebRequest()
+        {
+            var req = base.CreateWebRequest();
+
+            req.ContentType = "application/json";
+            req.Method = HttpMethod.Put;
+
+            req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings
+            {
+                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+            }));
+
+            return req;
+        }
+
+        protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}";
+    }
+}

From 6cb14e91c91043e1e3ddf0d2c999b9665def1d65 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 14:47:15 +0900
Subject: [PATCH 366/434] Make Player abstract and introduce SoloPlayer

---
 .../Visual/Navigation/TestScenePerformFromScreen.cs      | 4 ++--
 osu.Game/Screens/Play/Player.cs                          | 4 ++--
 osu.Game/Screens/Play/SoloPlayer.cs                      | 9 +++++++++
 osu.Game/Screens/Select/PlaySongSelect.cs                | 2 +-
 4 files changed, 14 insertions(+), 5 deletions(-)
 create mode 100644 osu.Game/Screens/Play/SoloPlayer.cs

diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 21d3bdaae3..2791952b66 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation
         public void TestPerformAtSongSelectFromPlayerLoader()
         {
             PushAndConfirm(() => new PlaySongSelect());
-            PushAndConfirm(() => new PlayerLoader(() => new Player()));
+            PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
 
             AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) }));
             AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Navigation
         public void TestPerformAtMenuFromPlayerLoader()
         {
             PushAndConfirm(() => new PlaySongSelect());
-            PushAndConfirm(() => new PlayerLoader(() => new Player()));
+            PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
 
             AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
             AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu);
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 0e221351aa..4cf0274614 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play
 {
     [Cached]
     [Cached(typeof(ISamplePlaybackDisabler))]
-    public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
+    public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
     {
         /// <summary>
         /// The delay upon completion of the beatmap before displaying the results screen.
@@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play
         /// <summary>
         /// Create a new player instance.
         /// </summary>
-        public Player(PlayerConfiguration configuration = null)
+        protected Player(PlayerConfiguration configuration = null)
         {
             Configuration = configuration ?? new PlayerConfiguration();
         }
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
new file mode 100644
index 0000000000..79860f3eda
--- /dev/null
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -0,0 +1,9 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Screens.Play
+{
+    public class SoloPlayer : Player
+    {
+    }
+}
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index e61d5cce85..dfb4b59060 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select
 
             SampleConfirm?.Play();
 
-            this.Push(player = new PlayerLoader(() => new Player()));
+            this.Push(player = new PlayerLoader(() => new SoloPlayer()));
 
             return true;
         }

From 7045fce55542b0f56a5907f0a87532bfef0728f3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 15:00:02 +0900
Subject: [PATCH 367/434] Move score submission logic in general out to its own
 Player type

---
 .../Multiplayer/MultiplayerPlayer.cs          |   1 -
 .../OnlinePlay/Playlists/PlaylistsPlayer.cs   |  74 ++-----------
 osu.Game/Screens/Play/SubmittingPlayer.cs     | 100 ++++++++++++++++++
 3 files changed, 106 insertions(+), 69 deletions(-)
 create mode 100644 osu.Game/Screens/Play/SubmittingPlayer.cs

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index b3cd44d55a..ef34d40497 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -19,7 +19,6 @@ using osuTK;
 
 namespace osu.Game.Screens.OnlinePlay.Multiplayer
 {
-    // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead.
     public class MultiplayerPlayer : PlaylistsPlayer
     {
         protected override bool PauseOnFocusLost => false;
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index 2b6dbd9dcb..a6aff5e43f 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -4,11 +4,8 @@
 using System;
 using System.Diagnostics;
 using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
-using osu.Framework.Logging;
 using osu.Framework.Screens;
 using osu.Game.Online.API;
 using osu.Game.Online.Rooms;
@@ -19,23 +16,12 @@ using osu.Game.Screens.Ranking;
 
 namespace osu.Game.Screens.OnlinePlay.Playlists
 {
-    public class PlaylistsPlayer : Player
+    public class PlaylistsPlayer : RoomSubmittingPlayer
     {
         public Action Exited;
 
-        [Resolved(typeof(Room), nameof(Room.RoomID))]
-        protected Bindable<long?> RoomId { get; private set; }
-
         protected readonly PlaylistItem PlaylistItem;
 
-        protected long? Token { get; private set; }
-
-        [Resolved]
-        private IAPIProvider api { get; set; }
-
-        [Resolved]
-        private IBindable<RulesetInfo> ruleset { get; set; }
-
         public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null)
             : base(configuration)
         {
@@ -43,12 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
         }
 
         [BackgroundDependencyLoader]
-        private void load()
+        private void load(IBindable<RulesetInfo> ruleset)
         {
-            Token = null;
-
-            bool failed = false;
-
             // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem
             if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID)
                 throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
@@ -58,31 +40,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
 
             if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
                 throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
-
-            var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
-            req.Success += r => Token = r.ID;
-            req.Failure += e =>
-            {
-                failed = true;
-
-                if (string.IsNullOrEmpty(e.Message))
-                    Logger.Error(e, "Failed to retrieve a score submission token.");
-                else
-                    Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important);
-
-                Schedule(() =>
-                {
-                    ValidForResume = false;
-                    this.Exit();
-                });
-            };
-
-            api.Queue(req);
-
-            while (!failed && !Token.HasValue)
-                Thread.Sleep(1000);
         }
 
+        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
+
+        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
+
         public override bool OnExiting(IScreen next)
         {
             if (base.OnExiting(next))
@@ -106,31 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
             return score;
         }
 
-        protected override async Task SubmitScore(Score score)
-        {
-            await base.SubmitScore(score).ConfigureAwait(false);
-
-            Debug.Assert(Token != null);
-
-            var tcs = new TaskCompletionSource<bool>();
-            var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
-
-            request.Success += s =>
-            {
-                score.ScoreInfo.OnlineScoreID = s.ID;
-                tcs.SetResult(true);
-            };
-
-            request.Failure += e =>
-            {
-                Logger.Error(e, "Failed to submit score");
-                tcs.SetResult(false);
-            };
-
-            api.Queue(request);
-            await tcs.Task.ConfigureAwait(false);
-        }
-
         protected override void Dispose(bool isDisposing)
         {
             base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
new file mode 100644
index 0000000000..250e308b3c
--- /dev/null
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+    public abstract class RoomSubmittingPlayer : SubmittingPlayer
+    {
+        [Resolved(typeof(Room), nameof(Room.RoomID))]
+        protected Bindable<long?> RoomId { get; private set; }
+
+        protected RoomSubmittingPlayer(PlayerConfiguration configuration)
+            : base(configuration)
+        {
+        }
+    }
+
+    public abstract class SubmittingPlayer : Player
+    {
+        protected long? Token { get; private set; }
+
+        [Resolved]
+        private IAPIProvider api { get; set; }
+
+        protected SubmittingPlayer(PlayerConfiguration configuration)
+            : base(configuration)
+        {
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            Token = null;
+
+            bool failed = false;
+
+            var req = CreateTokenRequestRequest();
+            req.Success += r => Token = r.ID;
+            req.Failure += e =>
+            {
+                failed = true;
+
+                if (string.IsNullOrEmpty(e.Message))
+                    Logger.Error(e, "Failed to retrieve a score submission token.");
+                else
+                    Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important);
+
+                Schedule(() =>
+                {
+                    ValidForResume = false;
+                    this.Exit();
+                });
+            };
+
+            api.Queue(req);
+
+            while (!failed && !Token.HasValue)
+                Thread.Sleep(1000);
+        }
+
+        protected override async Task SubmitScore(Score score)
+        {
+            await base.SubmitScore(score).ConfigureAwait(false);
+
+            Debug.Assert(Token != null);
+
+            var tcs = new TaskCompletionSource<bool>();
+            var request = CreateSubmissionRequest(score, Token.Value);
+
+            request.Success += s =>
+            {
+                score.ScoreInfo.OnlineScoreID = s.ID;
+                tcs.SetResult(true);
+            };
+
+            request.Failure += e =>
+            {
+                Logger.Error(e, "Failed to submit score");
+                tcs.SetResult(false);
+            };
+
+            api.Queue(request);
+            await tcs.Task.ConfigureAwait(false);
+        }
+
+        protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
+
+        protected abstract APIRequest<APIScoreToken> CreateTokenRequestRequest();
+    }
+}

From 12f050264aa31af09089a32d23472defe67a0ec5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 15:33:31 +0900
Subject: [PATCH 368/434] Further split out a player class which submits to
 "rooms"

---
 .../Multiplayer/MultiplayerPlayer.cs          |  3 +-
 .../OnlinePlay/Playlists/PlaylistsPlayer.cs   | 10 +-----
 osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 32 +++++++++++++++++++
 osu.Game/Screens/Play/SubmittingPlayer.cs     | 15 ++-------
 4 files changed, 37 insertions(+), 23 deletions(-)
 create mode 100644 osu.Game/Screens/Play/RoomSubmittingPlayer.cs

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index ef34d40497..2ba04b75d6 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Rooms;
 using osu.Game.Scoring;
-using osu.Game.Screens.OnlinePlay.Playlists;
 using osu.Game.Screens.Play;
 using osu.Game.Screens.Play.HUD;
 using osu.Game.Screens.Ranking;
@@ -19,7 +18,7 @@ using osuTK;
 
 namespace osu.Game.Screens.OnlinePlay.Multiplayer
 {
-    public class MultiplayerPlayer : PlaylistsPlayer
+    public class MultiplayerPlayer : RoomSubmittingPlayer
     {
         protected override bool PauseOnFocusLost => false;
 
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index a6aff5e43f..260d4961ff 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -7,7 +7,6 @@ using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Screens;
-using osu.Game.Online.API;
 using osu.Game.Online.Rooms;
 using osu.Game.Rulesets;
 using osu.Game.Scoring;
@@ -20,12 +19,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
     {
         public Action Exited;
 
-        protected readonly PlaylistItem PlaylistItem;
-
         public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null)
-            : base(configuration)
+            : base(playlistItem, configuration)
         {
-            PlaylistItem = playlistItem;
         }
 
         [BackgroundDependencyLoader]
@@ -42,10 +38,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
                 throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
         }
 
-        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
-
-        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
-
         public override bool OnExiting(IScreen next)
         {
             if (base.OnExiting(next))
diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
new file mode 100644
index 0000000000..c695e99874
--- /dev/null
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+    /// <summary>
+    /// A player instance which submits to a room backing. This is generally used by playlists and multiplayer.
+    /// </summary>
+    public abstract class RoomSubmittingPlayer : SubmittingPlayer
+    {
+        [Resolved(typeof(Room), nameof(Room.RoomID))]
+        protected Bindable<long?> RoomId { get; private set; }
+
+        protected readonly PlaylistItem PlaylistItem;
+
+        protected RoomSubmittingPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null)
+            : base(configuration)
+        {
+            PlaylistItem = playlistItem;
+        }
+
+        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
+
+        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
+    }
+}
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 250e308b3c..5577683f05 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -5,7 +5,6 @@ using System.Diagnostics;
 using System.Threading;
 using System.Threading.Tasks;
 using osu.Framework.Allocation;
-using osu.Framework.Bindables;
 using osu.Framework.Logging;
 using osu.Framework.Screens;
 using osu.Game.Online.API;
@@ -14,17 +13,9 @@ using osu.Game.Scoring;
 
 namespace osu.Game.Screens.Play
 {
-    public abstract class RoomSubmittingPlayer : SubmittingPlayer
-    {
-        [Resolved(typeof(Room), nameof(Room.RoomID))]
-        protected Bindable<long?> RoomId { get; private set; }
-
-        protected RoomSubmittingPlayer(PlayerConfiguration configuration)
-            : base(configuration)
-        {
-        }
-    }
-
+    /// <summary>
+    /// A player instance which supports submitting scores to an online store.
+    /// </summary>
     public abstract class SubmittingPlayer : Player
     {
         protected long? Token { get; private set; }

From 194b2d05d3749b6b60796c699490df6320c62d59 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 15:35:06 +0900
Subject: [PATCH 369/434] Update SoloPlayer to derive SubmittingPlayer

---
 osu.Game/Screens/Play/SoloPlayer.cs       | 15 ++++++++++++++-
 osu.Game/Screens/Play/SubmittingPlayer.cs |  2 +-
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index 79860f3eda..f2f97c5d0d 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -1,9 +1,22 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
 namespace osu.Game.Screens.Play
 {
-    public class SoloPlayer : Player
+    public class SoloPlayer : SubmittingPlayer
     {
+        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token)
+        {
+            throw new System.NotImplementedException();
+        }
+
+        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest()
+        {
+            throw new System.NotImplementedException();
+        }
     }
 }
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 5577683f05..e7847a9902 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play
         [Resolved]
         private IAPIProvider api { get; set; }
 
-        protected SubmittingPlayer(PlayerConfiguration configuration)
+        protected SubmittingPlayer(PlayerConfiguration configuration = null)
             : base(configuration)
         {
         }

From 571124669daf940ef35bc5a6a81cd8b64fa02bc7 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 15:45:22 +0900
Subject: [PATCH 370/434] Remove all references to "score submission" from
 Player

---
 .../Multiplayer/MultiplayerPlayer.cs          |  4 ++--
 osu.Game/Screens/Play/Player.cs               | 20 +++++++++----------
 osu.Game/Screens/Play/SubmittingPlayer.cs     |  4 ++--
 3 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 2ba04b75d6..3797adf360 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -133,9 +133,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
         private void onResultsReady() => resultsReady.SetResult(true);
 
-        protected override async Task SubmitScore(Score score)
+        protected override async Task PrepareScoreForResultsAsync(Score score)
         {
-            await base.SubmitScore(score).ConfigureAwait(false);
+            await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
 
             await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false);
 
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 4cf0274614..efe5d26409 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -559,7 +559,7 @@ namespace osu.Game.Screens.Play
         }
 
         private ScheduledDelegate completionProgressDelegate;
-        private Task<ScoreInfo> scoreSubmissionTask;
+        private Task<ScoreInfo> prepareScoreForDisplayTask;
 
         private void updateCompletionState(ValueChangedEvent<bool> completionState)
         {
@@ -586,17 +586,17 @@ namespace osu.Game.Screens.Play
 
             if (!Configuration.ShowResults) return;
 
-            scoreSubmissionTask ??= Task.Run(async () =>
+            prepareScoreForDisplayTask ??= Task.Run(async () =>
             {
                 var score = CreateScore();
 
                 try
                 {
-                    await SubmitScore(score).ConfigureAwait(false);
+                    await PrepareScoreForResultsAsync(score).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
-                    Logger.Error(ex, "Score submission failed!");
+                    Logger.Error(ex, "Score preparation failed!");
                 }
 
                 try
@@ -617,7 +617,7 @@ namespace osu.Game.Screens.Play
 
         private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
         {
-            if (!scoreSubmissionTask.IsCompleted)
+            if (!prepareScoreForDisplayTask.IsCompleted)
             {
                 scheduleCompletion();
                 return;
@@ -625,7 +625,7 @@ namespace osu.Game.Screens.Play
 
             // screen may be in the exiting transition phase.
             if (this.IsCurrentScreen())
-                this.Push(CreateResults(scoreSubmissionTask.Result));
+                this.Push(CreateResults(prepareScoreForDisplayTask.Result));
         });
 
         protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
@@ -895,11 +895,11 @@ namespace osu.Game.Screens.Play
         }
 
         /// <summary>
-        /// Submits the player's <see cref="Score"/>.
+        /// Prepare the <see cref="Score"/> for display at results.
         /// </summary>
-        /// <param name="score">The <see cref="Score"/> to submit.</param>
-        /// <returns>The submitted score.</returns>
-        protected virtual Task SubmitScore(Score score) => Task.CompletedTask;
+        /// <param name="score">The <see cref="Score"/> to prepare.</param>
+        /// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
+        protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
 
         /// <summary>
         /// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index e7847a9902..d876cad941 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -59,9 +59,9 @@ namespace osu.Game.Screens.Play
                 Thread.Sleep(1000);
         }
 
-        protected override async Task SubmitScore(Score score)
+        protected override async Task PrepareScoreForResultsAsync(Score score)
         {
-            await base.SubmitScore(score).ConfigureAwait(false);
+            await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
 
             Debug.Assert(Token != null);
 

From 3cd8bf2d7f30d2be87d7e67acdcae1a4cfef69f6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 16:05:40 +0900
Subject: [PATCH 371/434] Move token request construction to LoadAsyncComplete
 to better allow DI usage

---
 .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs     | 5 +++++
 osu.Game/Screens/Play/SubmittingPlayer.cs                   | 6 +++---
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 3797adf360..a5adcdb8ad 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -61,6 +61,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
             LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add);
 
             HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
+        }
+
+        protected override void LoadAsyncComplete()
+        {
+            base.LoadAsyncComplete();
 
             if (Token == null)
                 return; // Todo: Somehow handle token retrieval failure.
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index d876cad941..356f70f4bf 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -28,11 +28,11 @@ namespace osu.Game.Screens.Play
         {
         }
 
-        [BackgroundDependencyLoader]
-        private void load()
+        protected override void LoadAsyncComplete()
         {
-            Token = null;
+            base.LoadAsyncComplete();
 
+            // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
             bool failed = false;
 
             var req = CreateTokenRequestRequest();

From 242b847516cdf53f4f2c54fdd7097b9ec862f9a5 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 16:41:36 +0900
Subject: [PATCH 372/434] Add flow for allowing gameplay to continue even when
 an error occurs with token retrieval

---
 osu.Game/Screens/Play/RoomSubmittingPlayer.cs |  2 +-
 osu.Game/Screens/Play/SubmittingPlayer.cs     | 71 +++++++++++++------
 2 files changed, 51 insertions(+), 22 deletions(-)

diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
index c695e99874..6ef39e4b75 100644
--- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -27,6 +27,6 @@ namespace osu.Game.Screens.Play
 
         protected override APIRequest<APIScoreToken> CreateTokenRequestRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
 
-        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
+        protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
     }
 }
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 356f70f4bf..55f4ba4b9b 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -1,8 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System.Diagnostics;
-using System.Threading;
+using System;
 using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Logging;
@@ -30,40 +29,70 @@ namespace osu.Game.Screens.Play
 
         protected override void LoadAsyncComplete()
         {
-            base.LoadAsyncComplete();
-
             // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
-            bool failed = false;
+            var tcs = new TaskCompletionSource<bool>();
+
+            if (!api.IsLoggedIn)
+            {
+                fail(new InvalidOperationException("API is not online."));
+                return;
+            }
 
             var req = CreateTokenRequestRequest();
-            req.Success += r => Token = r.ID;
-            req.Failure += e =>
+
+            if (req == null)
             {
-                failed = true;
+                fail(new InvalidOperationException("Request could not be constructed."));
+                return;
+            }
 
-                if (string.IsNullOrEmpty(e.Message))
-                    Logger.Error(e, "Failed to retrieve a score submission token.");
-                else
-                    Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important);
-
-                Schedule(() =>
-                {
-                    ValidForResume = false;
-                    this.Exit();
-                });
+            req.Success += r =>
+            {
+                Token = r.ID;
+                tcs.SetResult(true);
             };
+            req.Failure += fail;
 
             api.Queue(req);
 
-            while (!failed && !Token.HasValue)
-                Thread.Sleep(1000);
+            tcs.Task.Wait();
+
+            void fail(Exception exception)
+            {
+                if (HandleTokenRetrievalFailure(exception))
+                {
+                    if (string.IsNullOrEmpty(exception.Message))
+                        Logger.Error(exception, "Failed to retrieve a score submission token.");
+                    else
+                        Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important);
+
+                    Schedule(() =>
+                    {
+                        ValidForResume = false;
+                        this.Exit();
+                    });
+                }
+
+                tcs.SetResult(false);
+            }
+
+            base.LoadAsyncComplete();
         }
 
+        /// <summary>
+        /// Called when a token could not be retrieved for submission.
+        /// </summary>
+        /// <param name="exception">The error causing the failure.</param>
+        /// <returns>Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.</returns>
+        protected virtual bool HandleTokenRetrievalFailure(Exception exception) => true;
+
         protected override async Task PrepareScoreForResultsAsync(Score score)
         {
             await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
 
-            Debug.Assert(Token != null);
+            // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
+            if (Token == null)
+                return;
 
             var tcs = new TaskCompletionSource<bool>();
             var request = CreateSubmissionRequest(score, Token.Value);

From e649a330a405d7d58490e50ea8c7f3b17cb3e27c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 16:41:52 +0900
Subject: [PATCH 373/434] Implement SoloPlayer's request construction

---
 osu.Game/Screens/Play/SoloPlayer.cs | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index f2f97c5d0d..3dc9df146e 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -1,22 +1,34 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
+using System.Diagnostics;
 using osu.Game.Online.API;
 using osu.Game.Online.Rooms;
+using osu.Game.Online.Solo;
 using osu.Game.Scoring;
 
 namespace osu.Game.Screens.Play
 {
     public class SoloPlayer : SubmittingPlayer
     {
-        public override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, int token)
+        protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
         {
-            throw new System.NotImplementedException();
+            Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
+
+            int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
+
+            return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
         }
 
         protected override APIRequest<APIScoreToken> CreateTokenRequestRequest()
         {
-            throw new System.NotImplementedException();
+            if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
+                return null;
+
+            return new CreateSoloScoreRequest(beatmapId, Game.VersionHash);
         }
+
+        protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
     }
 }

From 64e85ba995a20dcf1cd3eb6f95fc65ae1a47a0cb Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 19:19:07 +0900
Subject: [PATCH 374/434] Always fade out approach circles at a HitObject's
 start time to better match stable

---
 .../Objects/Drawables/DrawableHitCircle.cs    | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 77094f928b..189003875d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -164,28 +164,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
             ApproachCircle.Expire(true);
         }
 
+        protected override void UpdateStartTimeStateTransforms()
+        {
+            base.UpdateStartTimeStateTransforms();
+
+            ApproachCircle.FadeOut(50);
+        }
+
         protected override void UpdateHitStateTransforms(ArmedState state)
         {
             Debug.Assert(HitObject.HitWindows != null);
 
+            // todo: temporary / arbitrary, used for lifetime optimisation.
+            this.Delay(800).FadeOut();
+
             switch (state)
             {
                 case ArmedState.Idle:
-                    this.Delay(HitObject.TimePreempt).FadeOut(500);
                     HitArea.HitAction = null;
                     break;
 
                 case ArmedState.Miss:
-                    ApproachCircle.FadeOut(50);
                     this.FadeOut(100);
                     break;
-
-                case ArmedState.Hit:
-                    ApproachCircle.FadeOut(50);
-
-                    // todo: temporary / arbitrary
-                    this.Delay(800).FadeOut();
-                    break;
             }
 
             Expire();

From d10ff615feeadb9ece5d1f67863ddb9d26152bdc Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Tue, 23 Mar 2021 19:22:37 +0900
Subject: [PATCH 375/434] Fix default skin's glow resetting fade on miss

---
 osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
index fcbe4c1b28..46aeadc59b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
@@ -74,10 +74,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
 
         private void updateState(DrawableHitObject drawableObject, ArmedState state)
         {
-            using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
-            {
+            using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
                 glow.FadeOut(400);
 
+            using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+            {
                 switch (state)
                 {
                     case ArmedState.Hit:

From d17c431faf92affd103a85b46f0a79fdf633040e Mon Sep 17 00:00:00 2001
From: Shivam <s.r.v.ramkisoen@umail.leidenuniv.nl>
Date: Tue, 23 Mar 2021 23:22:17 +0100
Subject: [PATCH 376/434] Disable relative mode for TournamentGame

---
 osu.Game.Tournament/TournamentGame.cs | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index fadb821bef..bf43b198c4 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -2,18 +2,21 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Drawing;
-using osu.Framework.Extensions.Color4Extensions;
+using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Configuration;
+using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Colour;
-using osu.Game.Graphics.Cursor;
-using osu.Game.Tournament.Models;
+using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Platform;
 using osu.Game.Graphics;
+using osu.Game.Graphics.Cursor;
 using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Models;
 using osuTK;
 using osuTK.Graphics;
 
@@ -36,7 +39,7 @@ namespace osu.Game.Tournament
         private LoadingSpinner loadingSpinner;
 
         [BackgroundDependencyLoader]
-        private void load(FrameworkConfigManager frameworkConfig)
+        private void load(FrameworkConfigManager frameworkConfig, GameHost host)
         {
             windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
             windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
@@ -48,6 +51,9 @@ namespace osu.Game.Tournament
                 Margin = new MarginPadding(40),
             });
 
+            var m = (MouseHandler)host.AvailableInputHandlers.Single(t => t is MouseHandler);
+            m.UseRelativeMode.Value = false;
+
             loadingSpinner.Show();
 
             BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]

From fbb992fc7e432900ce9090d98446e6e3e18588ae Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Tue, 23 Mar 2021 19:18:32 -0500
Subject: [PATCH 377/434] Added a comment to new method

---
 osu.Game/OsuGame.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index a52899433a..e8284c0bad 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1008,6 +1008,7 @@ namespace osu.Game
                 Exit();
         }
         protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() {
+            // Overriding settings determined by Framework
             IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
             defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);
             return defaultOverrides;

From 67a03ebc2371c2bb6c14b721fd03f2627cb58b92 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Tue, 23 Mar 2021 19:31:16 -0500
Subject: [PATCH 378/434] Fixed formatting issues to be in line with osu coding
 standards

---
 osu.Game/OsuGame.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index e8284c0bad..ca8fa9f1f6 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1007,7 +1007,9 @@ namespace osu.Game
             if (newScreen == null)
                 Exit();
         }
-        protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() {
+
+        protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
+        {
             // Overriding settings determined by Framework
             IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
             defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);

From 437dadc85f0927936004abc72bc5331289d7e333 Mon Sep 17 00:00:00 2001
From: Owen Young <owen.young8699@gmail.com>
Date: Tue, 23 Mar 2021 19:37:55 -0500
Subject: [PATCH 379/434] Changed comment on GetFrameworkConfigDefaults() to be
 more accurate

---
 osu.Game/OsuGame.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index ca8fa9f1f6..f211723c59 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1010,7 +1010,7 @@ namespace osu.Game
 
         protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
         {
-            // Overriding settings determined by Framework
+            // Overriding config defaults determined by Framework
             IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
             defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);
             return defaultOverrides;

From a1c35677efb5e68f5691896b8ca6dfb3763b12e3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:02:17 +0900
Subject: [PATCH 380/434] Add more xmldoc

---
 osu.Game/Screens/Play/SubmittingPlayer.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 55f4ba4b9b..24d540fbf3 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -17,6 +17,9 @@ namespace osu.Game.Screens.Play
     /// </summary>
     public abstract class SubmittingPlayer : Player
     {
+        /// <summary>
+        /// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequestRequest"/>.
+        /// </summary>
         protected long? Token { get; private set; }
 
         [Resolved]

From 8bed7748d696666bfe0023f78710cf84440bc244 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:02:37 +0900
Subject: [PATCH 381/434] Rename token request method to avoid double Request
 terminology

---
 osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 2 +-
 osu.Game/Screens/Play/SoloPlayer.cs           | 2 +-
 osu.Game/Screens/Play/SubmittingPlayer.cs     | 6 +++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
index 6ef39e4b75..d7b49bf2cb 100644
--- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Screens.Play
             PlaylistItem = playlistItem;
         }
 
-        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
+        protected override APIRequest<APIScoreToken> CreateTokenRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
 
         protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
     }
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index 3dc9df146e..5c465b6d8f 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Screens.Play
             return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
         }
 
-        protected override APIRequest<APIScoreToken> CreateTokenRequestRequest()
+        protected override APIRequest<APIScoreToken> CreateTokenRequest()
         {
             if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
                 return null;
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 24d540fbf3..55b10abd5c 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play
     public abstract class SubmittingPlayer : Player
     {
         /// <summary>
-        /// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequestRequest"/>.
+        /// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequest"/>.
         /// </summary>
         protected long? Token { get; private set; }
 
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
                 return;
             }
 
-            var req = CreateTokenRequestRequest();
+            var req = CreateTokenRequest();
 
             if (req == null)
             {
@@ -118,6 +118,6 @@ namespace osu.Game.Screens.Play
 
         protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
 
-        protected abstract APIRequest<APIScoreToken> CreateTokenRequestRequest();
+        protected abstract APIRequest<APIScoreToken> CreateTokenRequest();
     }
 }

From e372e355efba030da6866f61fad35abca462cd6d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:12:51 +0900
Subject: [PATCH 382/434] Reorder overrides in SoloPlayer to better follow
 chronological request order

---
 osu.Game/Screens/Play/SoloPlayer.cs | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index 5c465b6d8f..ee1ccdc5b3 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -12,15 +12,6 @@ namespace osu.Game.Screens.Play
 {
     public class SoloPlayer : SubmittingPlayer
     {
-        protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
-        {
-            Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
-
-            int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
-
-            return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
-        }
-
         protected override APIRequest<APIScoreToken> CreateTokenRequest()
         {
             if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
@@ -30,5 +21,14 @@ namespace osu.Game.Screens.Play
         }
 
         protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
+
+        protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
+        {
+            Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
+
+            int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
+
+            return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
+        }
     }
 }

From a0c6c4da35dfe5cf2a28efa20da64acab28f25ab Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:17:13 +0900
Subject: [PATCH 383/434] Rename and refactor token request process to be
 easier to understand

---
 osu.Game/Screens/Play/SubmittingPlayer.cs | 24 +++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 55b10abd5c..5d087c212d 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Threading.Tasks;
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Logging;
 using osu.Framework.Screens;
@@ -37,7 +38,7 @@ namespace osu.Game.Screens.Play
 
             if (!api.IsLoggedIn)
             {
-                fail(new InvalidOperationException("API is not online."));
+                handleFailure(new InvalidOperationException("API is not online."));
                 return;
             }
 
@@ -45,7 +46,7 @@ namespace osu.Game.Screens.Play
 
             if (req == null)
             {
-                fail(new InvalidOperationException("Request could not be constructed."));
+                handleFailure(new InvalidOperationException("Request could not be constructed."));
                 return;
             }
 
@@ -54,13 +55,13 @@ namespace osu.Game.Screens.Play
                 Token = r.ID;
                 tcs.SetResult(true);
             };
-            req.Failure += fail;
+            req.Failure += handleFailure;
 
             api.Queue(req);
 
             tcs.Task.Wait();
 
-            void fail(Exception exception)
+            void handleFailure(Exception exception)
             {
                 if (HandleTokenRetrievalFailure(exception))
                 {
@@ -116,8 +117,19 @@ namespace osu.Game.Screens.Play
             await tcs.Task.ConfigureAwait(false);
         }
 
-        protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
-
+        /// <summary>
+        /// Construct a request to be used for retrieval of the score token.
+        /// Can return null, at which point <see cref="HandleTokenRetrievalFailure"/> will be fired.
+        /// </summary>
+        [CanBeNull]
         protected abstract APIRequest<APIScoreToken> CreateTokenRequest();
+
+        /// <summary>
+        /// Construct a request to submit the score.
+        /// Will only be invoked if the request constructed via <see cref="CreateTokenRequest"/> was successful.
+        /// </summary>
+        /// <param name="score">The score to be submitted.</param>
+        /// <param name="token">The submission token.</param>
+        protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
     }
 }

From 84b2f9a848c0c61b047f5eb463e7848cdbd9b03a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:20:44 +0900
Subject: [PATCH 384/434] Make token private

---
 .../Multiplayer/MultiplayerPlayer.cs          |  4 ++--
 osu.Game/Screens/Play/SubmittingPlayer.cs     | 20 +++++++++----------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index a5adcdb8ad..aaacf891bb 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -67,8 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
         {
             base.LoadAsyncComplete();
 
-            if (Token == null)
-                return; // Todo: Somehow handle token retrieval failure.
+            if (!ValidForResume)
+                return; // token retrieval may have failed.
 
             client.MatchStarted += onMatchStarted;
             client.ResultsReady += onResultsReady;
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 5d087c212d..87a4eb5efe 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Screens.Play
         /// <summary>
         /// The token to be used for the current submission. This is fetched via a request created by <see cref="CreateTokenRequest"/>.
         /// </summary>
-        protected long? Token { get; private set; }
+        private long? token;
 
         [Resolved]
         private IAPIProvider api { get; set; }
@@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play
 
             if (!api.IsLoggedIn)
             {
-                handleFailure(new InvalidOperationException("API is not online."));
+                handleTokenFailure(new InvalidOperationException("API is not online."));
                 return;
             }
 
@@ -46,22 +46,24 @@ namespace osu.Game.Screens.Play
 
             if (req == null)
             {
-                handleFailure(new InvalidOperationException("Request could not be constructed."));
+                handleTokenFailure(new InvalidOperationException("Request could not be constructed."));
                 return;
             }
 
             req.Success += r =>
             {
-                Token = r.ID;
+                token = r.ID;
                 tcs.SetResult(true);
             };
-            req.Failure += handleFailure;
+            req.Failure += handleTokenFailure;
 
             api.Queue(req);
 
             tcs.Task.Wait();
 
-            void handleFailure(Exception exception)
+            base.LoadAsyncComplete();
+
+            void handleTokenFailure(Exception exception)
             {
                 if (HandleTokenRetrievalFailure(exception))
                 {
@@ -79,8 +81,6 @@ namespace osu.Game.Screens.Play
 
                 tcs.SetResult(false);
             }
-
-            base.LoadAsyncComplete();
         }
 
         /// <summary>
@@ -95,11 +95,11 @@ namespace osu.Game.Screens.Play
             await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
 
             // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
-            if (Token == null)
+            if (token == null)
                 return;
 
             var tcs = new TaskCompletionSource<bool>();
-            var request = CreateSubmissionRequest(score, Token.Value);
+            var request = CreateSubmissionRequest(score, token.Value);
 
             request.Success += s =>
             {

From d55324585d678603435adcaed815d3d85186037b Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:23:23 +0900
Subject: [PATCH 385/434] Change RoomSubmittingPlayer's request implementation
 to return null on RoomID missing, rather than silently succeeding

---
 osu.Game/Screens/Play/RoomSubmittingPlayer.cs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
index d7b49bf2cb..7ba12f5db6 100644
--- a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -25,7 +25,13 @@ namespace osu.Game.Screens.Play
             PlaylistItem = playlistItem;
         }
 
-        protected override APIRequest<APIScoreToken> CreateTokenRequest() => new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
+        protected override APIRequest<APIScoreToken> CreateTokenRequest()
+        {
+            if (!(RoomId.Value is long roomId))
+                return null;
+
+            return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash);
+        }
 
         protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
     }

From f95175983ae6f4f79a2a27877467e8ef40f5cb22 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 13:37:37 +0900
Subject: [PATCH 386/434] Make code more concise and move method to a more
 appropriate place

---
 osu.Game/OsuGame.cs | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 53dc900254..dd1fa32ad9 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -531,6 +531,13 @@ namespace osu.Game
             SentryLogger.Dispose();
         }
 
+        protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
+            => new Dictionary<FrameworkSetting, object>
+            {
+                // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance)
+                { FrameworkSetting.WindowMode, WindowMode.Fullscreen }
+            };
+
         protected override void LoadComplete()
         {
             base.LoadComplete();
@@ -1013,13 +1020,5 @@ namespace osu.Game
             if (newScreen == null)
                 Exit();
         }
-
-        protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
-        {
-            // Overriding config defaults determined by Framework
-            IDictionary<FrameworkSetting, object> defaultOverrides = new Dictionary<FrameworkSetting, object>();
-            defaultOverrides.Add(FrameworkSetting.WindowMode, WindowMode.Fullscreen);
-            return defaultOverrides;
-        }
     }
 }

From 5ad8dc316fa1aeed2f2dc194c2f9520b7f93ba09 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Wed, 24 Mar 2021 14:09:15 +0900
Subject: [PATCH 387/434] Add inline comment and improve linq robustness

---
 osu.Game.Tournament/TournamentGame.cs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index bf43b198c4..87e23e3404 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -51,8 +51,12 @@ namespace osu.Game.Tournament
                 Margin = new MarginPadding(40),
             });
 
-            var m = (MouseHandler)host.AvailableInputHandlers.Single(t => t is MouseHandler);
-            m.UseRelativeMode.Value = false;
+            // in order to have the OS mouse cursor visible, relative mode needs to be disabled.
+            // can potentially be removed when https://github.com/ppy/osu-framework/issues/4309 is resolved.
+            var mouseHandler = host.AvailableInputHandlers.OfType<MouseHandler>().FirstOrDefault();
+
+            if (mouseHandler != null)
+                mouseHandler.UseRelativeMode.Value = false;
 
             loadingSpinner.Show();
 

From fc5719e445750c10ddad28ead84239a9cb0f519d Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Wed, 24 Mar 2021 21:31:53 +0300
Subject: [PATCH 388/434] Fix SkinManager not handling extensions casing
 comparsion properly

---
 osu.Game/Skinning/SkinManager.cs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 9257636301..dfc2981c6f 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -104,7 +104,7 @@ namespace osu.Game.Skinning
         protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
         {
             // we need to populate early to create a hash based off skin.ini contents
-            if (item.Name?.Contains(".osk") == true)
+            if (item.Name?.EndsWith(".osk", StringComparison.OrdinalIgnoreCase) == true)
                 populateMetadata(item);
 
             if (item.Creator != null && item.Creator != unknown_creator_string)
@@ -122,7 +122,7 @@ namespace osu.Game.Skinning
         {
             await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
 
-            if (model.Name?.Contains(".osk") == true)
+            if (model.Name?.EndsWith(".osk", StringComparison.OrdinalIgnoreCase) == true)
                 populateMetadata(model);
         }
 
@@ -137,7 +137,7 @@ namespace osu.Game.Skinning
             }
             else
             {
-                item.Name = item.Name.Replace(".osk", "");
+                item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
                 item.Creator ??= unknown_creator_string;
             }
         }

From 35810bb2fb98b7f310d2e19b29a8369e1ddb99a8 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Wed, 24 Mar 2021 22:55:15 +0300
Subject: [PATCH 389/434] Add test coverage

---
 osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 25 +++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index a5b4b04ef5..8124bd4199 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -113,6 +113,31 @@ namespace osu.Game.Tests.Skins.IO
             }
         }
 
+        [Test]
+        public async Task TestImportUpperCasedOskArchive()
+        {
+            using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
+            {
+                try
+                {
+                    var osu = LoadOsuIntoHost(host);
+
+                    var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.OsK"));
+
+                    Assert.That(imported.Name, Is.EqualTo("name 1"));
+                    Assert.That(imported.Creator, Is.EqualTo("author 1"));
+
+                    var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.oSK"));
+
+                    Assert.That(imported2.Hash, Is.EqualTo(imported.Hash));
+                }
+                finally
+                {
+                    host.Exit();
+                }
+            }
+        }
+
         private MemoryStream createOsk(string name, string author)
         {
             var zipStream = new MemoryStream();

From 8753d45b71baab76b83f6245e5e8ad98c3ea3e92 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 12:32:51 +0900
Subject: [PATCH 390/434] Remove duplicate crash report issue template

---
 .github/ISSUE_TEMPLATE/02-crash-issues.md     | 20 -------------------
 ...issues.md => 02-feature-request-issues.md} |  2 +-
 2 files changed, 1 insertion(+), 21 deletions(-)
 delete mode 100644 .github/ISSUE_TEMPLATE/02-crash-issues.md
 rename .github/ISSUE_TEMPLATE/{03-feature-request-issues.md => 02-feature-request-issues.md} (62%)

diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md
deleted file mode 100644
index 04170312d1..0000000000
--- a/.github/ISSUE_TEMPLATE/02-crash-issues.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Crash Report
-about: Issues regarding crashes or permanent freezes.
----
-**Describe the crash:** 
-
-**Screenshots or videos showing encountered issue:** 
-
-**osu!lazer version:** 
-
-**Logs:**
-<!--
-*please attach logs here, which are located at:*
-- `%AppData%/osu/logs` *(on Windows),*
-- `~/.local/share/osu/logs` *(on Linux & macOS).*
-- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
-- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
--->
-
-**Computer Specifications:** 
diff --git a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
similarity index 62%
rename from .github/ISSUE_TEMPLATE/03-feature-request-issues.md
rename to .github/ISSUE_TEMPLATE/02-feature-request-issues.md
index 54c4ff94e5..c3357dd780 100644
--- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
@@ -1,6 +1,6 @@
 ---
 name: Feature Request
-about: Features you would like to see in the game!
+about: Propose a feature you would like to see in the game!
 ---
 **Describe the new feature:** 
 

From f8f461a7a4b2c50edca8d0a2f3c9318d244394a9 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 12:33:07 +0900
Subject: [PATCH 391/434] Include blurb in issue template to hopefully help
 people find existing reports

---
 .github/ISSUE_TEMPLATE/01-bug-issues.md | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 6050036cbf..e45893b97a 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -1,7 +1,18 @@
 ---
 name: Bug Report
-about: Issues regarding encountered bugs.
+about: Report a bug or crash to desktop
 ---
+
+<!--
+IMPORTANT: Your issue may already be reported.
+
+Please check:
+- Pinned issues, at the top of https://github.com/ppy/osu/issues
+- Current priority 0 issues at https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0
+- Search for your issue. If you find that it already exists, please respond with a reaction or add any further information that may be helpful.
+-->
+
+
 **Describe the bug:** 
 
 **Screenshots or videos showing encountered issue:** 
@@ -9,6 +20,7 @@ about: Issues regarding encountered bugs.
 **osu!lazer version:** 
 
 **Logs:**
+
 <!--
 *please attach logs here, which are located at:*
 - `%AppData%/osu/logs` *(on Windows),*

From e22650293dda921827d34f8beb49b12ea7ab04ca Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 13:35:42 +0900
Subject: [PATCH 392/434] Remove unnecessary second call to StripComments in
 skin parsing logic

---
 osu.Game/Skinning/LegacySkinDecoder.cs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs
index 75b7ba28b9..2700f84815 100644
--- a/osu.Game/Skinning/LegacySkinDecoder.cs
+++ b/osu.Game/Skinning/LegacySkinDecoder.cs
@@ -17,8 +17,6 @@ namespace osu.Game.Skinning
         {
             if (section != Section.Colours)
             {
-                line = StripComments(line);
-
                 var pair = SplitKeyVal(line);
 
                 switch (section)

From 4f8edcd336c31694ad04c75d661f5c1873f0cf14 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 13:35:54 +0900
Subject: [PATCH 393/434] Don't strip comments from metadata during parsin

---
 osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 10a716963e..b39890084f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -36,7 +36,13 @@ namespace osu.Game.Beatmaps.Formats
                 if (ShouldSkipLine(line))
                     continue;
 
-                line = StripComments(line).TrimEnd();
+                if (section != Section.Metadata)
+                {
+                    // comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data.
+                    line = StripComments(line);
+                }
+
+                line = line.TrimEnd();
 
                 if (line.StartsWith('[') && line.EndsWith(']'))
                 {

From 4269cb7124804eae7364a6efccf0f33c2265912d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 13:48:41 +0900
Subject: [PATCH 394/434] Extract majority of token retrieval code out of
 LoadComponentAsync for legibility

---
 osu.Game/Screens/Play/SubmittingPlayer.cs | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 87a4eb5efe..d22199447d 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -32,6 +32,13 @@ namespace osu.Game.Screens.Play
         }
 
         protected override void LoadAsyncComplete()
+        {
+            if (!handleTokenRetrieval()) return;
+
+            base.LoadAsyncComplete();
+        }
+
+        private bool handleTokenRetrieval()
         {
             // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
             var tcs = new TaskCompletionSource<bool>();
@@ -39,7 +46,7 @@ namespace osu.Game.Screens.Play
             if (!api.IsLoggedIn)
             {
                 handleTokenFailure(new InvalidOperationException("API is not online."));
-                return;
+                return false;
             }
 
             var req = CreateTokenRequest();
@@ -47,7 +54,7 @@ namespace osu.Game.Screens.Play
             if (req == null)
             {
                 handleTokenFailure(new InvalidOperationException("Request could not be constructed."));
-                return;
+                return false;
             }
 
             req.Success += r =>
@@ -60,8 +67,7 @@ namespace osu.Game.Screens.Play
             api.Queue(req);
 
             tcs.Task.Wait();
-
-            base.LoadAsyncComplete();
+            return true;
 
             void handleTokenFailure(Exception exception)
             {

From 0324bfa857efcc4ac3b4b019e41da300d6e92edd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 14:14:38 +0900
Subject: [PATCH 395/434] Change checks back to `Contains`

---
 osu.Game/Skinning/SkinManager.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index dfc2981c6f..ac4d63159a 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -104,7 +104,7 @@ namespace osu.Game.Skinning
         protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
         {
             // we need to populate early to create a hash based off skin.ini contents
-            if (item.Name?.EndsWith(".osk", StringComparison.OrdinalIgnoreCase) == true)
+            if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
                 populateMetadata(item);
 
             if (item.Creator != null && item.Creator != unknown_creator_string)
@@ -122,7 +122,7 @@ namespace osu.Game.Skinning
         {
             await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
 
-            if (model.Name?.EndsWith(".osk", StringComparison.OrdinalIgnoreCase) == true)
+            if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
                 populateMetadata(model);
         }
 

From 7b3336783fb3e4864680fa69d2ea8240d3ac93b1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 15:24:59 +0900
Subject: [PATCH 396/434] Stabilise ordering instead of simple reversing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
---
 .../Compose/Components/Timeline/TimelineBlueprintContainer.cs  | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 3526f264a7..09da613a04 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -138,8 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
 
             Stack<double> currentConcurrentObjects = new Stack<double>();
 
-            // Reversing is done to enumerate in order of increasing StartTime.
-            foreach (var b in SelectionBlueprints.Reverse())
+            foreach (var b in SelectionBlueprints.OrderBy(b => b.HitObject.StartTime).ThenBy(b => b.HitObject.GetEndTime()))
             {
                 while (currentConcurrentObjects.TryPeek(out double stackEndTime))
                 {

From 9fdd23b134744140c66a5e8595fb1d655c038f83 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 16:28:30 +0900
Subject: [PATCH 397/434] Fix various issues with stacking

---
 .../Timeline/TimelineBlueprintContainer.cs    | 27 ++++++++++---------
 1 file changed, 14 insertions(+), 13 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 09da613a04..cec851b5bb 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -136,41 +136,42 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
             // after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints.
             const int stack_reset_count = 3;
 
-            Stack<double> currentConcurrentObjects = new Stack<double>();
+            Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
 
             foreach (var b in SelectionBlueprints.OrderBy(b => b.HitObject.StartTime).ThenBy(b => b.HitObject.GetEndTime()))
             {
-                while (currentConcurrentObjects.TryPeek(out double stackEndTime))
+                // remove objects from the stack as long as their end time is in the past.
+                while (currentConcurrentObjects.TryPeek(out HitObject hitObject))
                 {
-                    if (Precision.AlmostBigger(stackEndTime, b.HitObject.StartTime, 1))
+                    if (Precision.AlmostBigger(hitObject.GetEndTime(), b.HitObject.StartTime, 1))
                         break;
 
                     currentConcurrentObjects.Pop();
                 }
 
-                b.Y = -(stack_offset * currentConcurrentObjects.Count);
-
-                var bEndTime = b.HitObject.GetEndTime();
-
                 // if the stack gets too high, we should have space below it to display the next batch of objects.
                 // importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves.
-                if (!currentConcurrentObjects.TryPeek(out double nextStackEndTime) ||
-                    !Precision.AlmostEquals(nextStackEndTime, bEndTime, 1))
+                if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.HitObject.StartTime, 1))
                 {
                     if (currentConcurrentObjects.Count >= stack_reset_count)
                         currentConcurrentObjects.Clear();
                 }
 
-                currentConcurrentObjects.Push(bEndTime);
+                b.Y = -(stack_offset * currentConcurrentObjects.Count);
+
+                currentConcurrentObjects.Push(b.HitObject);
             }
         }
 
         protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
 
-        protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject)
+        protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
         {
-            OnDragHandled = handleScrollViaDrag
-        };
+            return new TimelineHitObjectBlueprint(hitObject)
+            {
+                OnDragHandled = handleScrollViaDrag
+            };
+        }
 
         protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new TimelineDragBox(performSelect);
 

From 8d4ff867bfe7000b08e0abbe0966a1aa9045a72e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 16:30:31 +0900
Subject: [PATCH 398/434] Fix barlines showing up in the osu!mania editor's
 timeline

---
 osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 7def7e1d16..5699be4560 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -268,6 +268,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
 
         private void addBlueprintFor(HitObject hitObject)
         {
+            if (hitObject is IBarLine)
+                return;
+
             if (blueprintMap.ContainsKey(hitObject))
                 return;
 

From cb4ae6e61a87745a3204baa7459c5f895dc5b9e1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 17:34:29 +0900
Subject: [PATCH 399/434] Fix very high accuracies feigning as "perfect" 100%
 plays when actually below

---
 osu.Game/Users/UserStatistics.cs |  2 +-
 osu.Game/Utils/FormatUtils.cs    | 15 ++++++++-------
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs
index dc926898fc..04a358436e 100644
--- a/osu.Game/Users/UserStatistics.cs
+++ b/osu.Game/Users/UserStatistics.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Users
         public long RankedScore;
 
         [JsonProperty(@"hit_accuracy")]
-        public decimal Accuracy;
+        public double Accuracy;
 
         [JsonIgnore]
         public string DisplayAccuracy => Accuracy.FormatAccuracy();
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index 2578d8d835..70a96367be 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using Humanizer;
 
 namespace osu.Game.Utils
@@ -12,14 +13,14 @@ namespace osu.Game.Utils
         /// </summary>
         /// <param name="accuracy">The accuracy to be formatted</param>
         /// <returns>formatted accuracy in percentage</returns>
-        public static string FormatAccuracy(this double accuracy) => $"{accuracy:0.00%}";
+        public static string FormatAccuracy(this double accuracy)
+        {
+            // we don't ever want to show 100% when the accuracy is below perfect, even if it rounds to 100%.
+            if (accuracy < 1 && accuracy > 0.9999)
+                accuracy = 0.9999;
 
-        /// <summary>
-        /// Turns the provided accuracy into a percentage with 2 decimal places.
-        /// </summary>
-        /// <param name="accuracy">The accuracy to be formatted</param>
-        /// <returns>formatted accuracy in percentage</returns>
-        public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%";
+            return $"{accuracy:0.00%}";
+        }
 
         /// <summary>
         /// Formats the supplied rank/leaderboard position in a consistent, simplified way.

From 544117a49495a97c297f0253b0fcfa1725b9ce74 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 17:34:41 +0900
Subject: [PATCH 400/434] Add test coverage of accuracy formatting function

---
 osu.Game.Tests/NonVisual/FormatUtilsTest.cs | 25 +++++++++++++++++++++
 1 file changed, 25 insertions(+)
 create mode 100644 osu.Game.Tests/NonVisual/FormatUtilsTest.cs

diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
new file mode 100644
index 0000000000..eb738f5f67
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Utils;
+
+namespace osu.Game.Tests.NonVisual
+{
+    [TestFixture]
+    public class FormatUtilsTest
+    {
+        [TestCase(0, "0.00%")]
+        [TestCase(0.01, "1.00%")]
+        [TestCase(0.9899, "98.99%")]
+        [TestCase(0.989999, "99.00%")]
+        [TestCase(0.99, "99.00%")]
+        [TestCase(0.9999, "99.99%")]
+        [TestCase(0.999999, "99.99%")]
+        [TestCase(1, "100.00%")]
+        public void TestAccuracyFormatting(double input, string expectedOutput)
+        {
+            Assert.AreEqual(expectedOutput, input.FormatAccuracy());
+        }
+    }
+}

From 701342e03673b4e9d647484a4193418fbf64d107 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 17:43:51 +0900
Subject: [PATCH 401/434] Remove accuracy rounding at a ScoreProcessor level

---
 osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 2024290460..b81fa79345 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -337,7 +337,7 @@ namespace osu.Game.Rulesets.Scoring
             score.TotalScore = (long)Math.Round(GetStandardisedScore());
             score.Combo = Combo.Value;
             score.MaxCombo = HighestCombo.Value;
-            score.Accuracy = Math.Round(Accuracy.Value, 4);
+            score.Accuracy = Accuracy.Value;
             score.Rank = Rank.Value;
             score.Date = DateTimeOffset.Now;
 

From 630faa3b56e5a3b36577c695f906d006088e61e3 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 17:50:21 +0900
Subject: [PATCH 402/434] Add TODO marking incorrect EF core data type

As mentioned in the comment, we probably don't need to be storing this
in the database in the first place (as it should be able to be
calculated from the other statistics we have available to us). Something
to consider when we refactor the database backend.
---
 osu.Game/Scoring/ScoreInfo.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index f5192f3a40..ef11c19e3f 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Scoring
         public long TotalScore { get; set; }
 
         [JsonProperty("accuracy")]
-        [Column(TypeName = "DECIMAL(1,4)")]
+        [Column(TypeName = "DECIMAL(1,4)")] // TODO: This data type is wrong (should contain more precision). But at the same time, we probably don't need to be storing this in the database.
         public double Accuracy { get; set; }
 
         [JsonIgnore]

From 77888ae6402c669a0f6330d698b6794bc9ff5539 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 17:51:20 +0900
Subject: [PATCH 403/434] Remove unnecessary using

---
 osu.Game/Utils/FormatUtils.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index 70a96367be..4c7702f04e 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
 using Humanizer;
 
 namespace osu.Game.Utils

From b4bccc19baaddaaa2c3f643da5ff15eb822b9c7d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 25 Mar 2021 18:37:13 +0900
Subject: [PATCH 404/434] Refactor LegacySpriteText initialisation

---
 .../Skinning/Legacy/LegacySpinner.cs           | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 6d4fbd7445..dd1c6cad77 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -76,18 +76,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                 }
             });
 
-            bonusCounter = (LegacySpriteText)source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText));
+            bonusCounter = (source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as LegacySpriteText)?.With(c =>
+            {
+                c.Alpha = 0f;
+                c.Anchor = Anchor.TopCentre;
+                c.Origin = Anchor.Centre;
+                c.Font = c.Font.With(fixedWidth: false);
+                c.Scale = new Vector2(SPRITE_SCALE);
+                c.Y = SPINNER_TOP_OFFSET + 299;
+            });
 
             if (bonusCounter != null)
-            {
-                bonusCounter.Alpha = 0f;
-                bonusCounter.Anchor = Anchor.TopCentre;
-                bonusCounter.Origin = Anchor.Centre;
-                bonusCounter.Font = bonusCounter.Font.With(fixedWidth: false);
-                bonusCounter.Scale = new Vector2(SPRITE_SCALE);
-                bonusCounter.Y = SPINNER_TOP_OFFSET + 299;
                 overlayContainer.Add(bonusCounter);
-            }
         }
 
         private IBindable<double> gainedBonus;

From bada1e7189973f09452ad51419abbb79650f9972 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Thu, 25 Mar 2021 15:00:06 +0300
Subject: [PATCH 405/434] Update legacy spinner bonus counter usage

---
 .../Skinning/Legacy/LegacySpinner.cs          | 40 +++++++------------
 1 file changed, 15 insertions(+), 25 deletions(-)

diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index dd1c6cad77..064b7a4680 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -48,9 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 
             DrawableSpinner = (DrawableSpinner)drawableHitObject;
 
-            Container overlayContainer;
-
-            AddInternal(overlayContainer = new Container
+            AddInternal(new Container
             {
                 Depth = float.MinValue,
                 RelativeSizeAxes = Axes.Both,
@@ -73,21 +71,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
                         Scale = new Vector2(SPRITE_SCALE),
                         Y = SPINNER_TOP_OFFSET + 115,
                     },
+                    bonusCounter = new LegacySpriteText(source, LegacyFont.Score)
+                    {
+                        Alpha = 0f,
+                        Anchor = Anchor.TopCentre,
+                        Origin = Anchor.Centre,
+                        Scale = new Vector2(SPRITE_SCALE),
+                        Y = SPINNER_TOP_OFFSET + 299,
+                    }.With(s => s.Font = s.Font.With(fixedWidth: false)),
                 }
             });
-
-            bonusCounter = (source.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as LegacySpriteText)?.With(c =>
-            {
-                c.Alpha = 0f;
-                c.Anchor = Anchor.TopCentre;
-                c.Origin = Anchor.Centre;
-                c.Font = c.Font.With(fixedWidth: false);
-                c.Scale = new Vector2(SPRITE_SCALE);
-                c.Y = SPINNER_TOP_OFFSET + 299;
-            });
-
-            if (bonusCounter != null)
-                overlayContainer.Add(bonusCounter);
         }
 
         private IBindable<double> gainedBonus;
@@ -98,16 +91,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         {
             base.LoadComplete();
 
-            if (bonusCounter != null)
+            gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
+            gainedBonus.BindValueChanged(bonus =>
             {
-                gainedBonus = DrawableSpinner.GainedBonus.GetBoundCopy();
-                gainedBonus.BindValueChanged(bonus =>
-                {
-                    bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
-                    bonusCounter.FadeOutFromOne(800, Easing.Out);
-                    bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
-                });
-            }
+                bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
+                bonusCounter.FadeOutFromOne(800, Easing.Out);
+                bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
+            });
 
             completed.BindValueChanged(onCompletedChanged, true);
 

From aa2c1ee85ed80ff380b8868606a12169dcbfe758 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Max=20H=C3=BCbner?= <maxshuebner@gmail.com>
Date: Thu, 25 Mar 2021 23:20:10 +0100
Subject: [PATCH 406/434] Add new beatmap search filter row "General"

---
 .../API/Requests/SearchBeatmapSetsRequest.cs  |  8 ++++++++
 .../BeatmapListingFilterControl.cs            |  2 ++
 .../BeatmapListingSearchControl.cs            |  4 ++++
 .../Overlays/BeatmapListing/SearchGeneral.cs  | 19 +++++++++++++++++++
 4 files changed, 33 insertions(+)
 create mode 100644 osu.Game/Overlays/BeatmapListing/SearchGeneral.cs

diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 5360d36f3d..f1cb02fb10 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Online.API.Requests
 {
     public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
     {
+        [CanBeNull]
+        public IReadOnlyCollection<SearchGeneral> General { get; }
+
         public SearchCategory SearchCategory { get; }
 
         public SortCriteria SortCriteria { get; }
@@ -45,6 +48,7 @@ namespace osu.Game.Online.API.Requests
             string query,
             RulesetInfo ruleset,
             Cursor cursor = null,
+            IReadOnlyCollection<SearchGeneral> general = null,
             SearchCategory searchCategory = SearchCategory.Any,
             SortCriteria sortCriteria = SortCriteria.Ranked,
             SortDirection sortDirection = SortDirection.Descending,
@@ -59,6 +63,7 @@ namespace osu.Game.Online.API.Requests
             this.ruleset = ruleset;
             this.cursor = cursor;
 
+            General = general;
             SearchCategory = searchCategory;
             SortCriteria = sortCriteria;
             SortDirection = sortDirection;
@@ -75,6 +80,9 @@ namespace osu.Game.Online.API.Requests
             var req = base.CreateWebRequest();
             req.AddParameter("q", query);
 
+            if (General != null && General.Any())
+                req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToLowerInvariant())));
+
             if (ruleset.ID.HasValue)
                 req.AddParameter("m", ruleset.ID.Value.ToString());
 
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index bcc5a91677..1935a250b7 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -134,6 +134,7 @@ namespace osu.Game.Overlays.BeatmapListing
                 queueUpdateSearch(true);
             });
 
+            searchControl.General.CollectionChanged += (_, __) => queueUpdateSearch();
             searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch());
             searchControl.Category.BindValueChanged(_ => queueUpdateSearch());
             searchControl.Genre.BindValueChanged(_ => queueUpdateSearch());
@@ -187,6 +188,7 @@ namespace osu.Game.Overlays.BeatmapListing
                 searchControl.Query.Value,
                 searchControl.Ruleset.Value,
                 lastResponse?.Cursor,
+                searchControl.General,
                 searchControl.Category.Value,
                 sortControl.Current.Value,
                 sortControl.SortDirection.Value,
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index b138a5ac52..1576431d40 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Overlays.BeatmapListing
 
         public Bindable<string> Query => textBox.Current;
 
+        public BindableList<SearchGeneral> General => generalFilter.Current;
+
         public Bindable<RulesetInfo> Ruleset => modeFilter.Current;
 
         public Bindable<SearchCategory> Category => categoryFilter.Current;
@@ -61,6 +63,7 @@ namespace osu.Game.Overlays.BeatmapListing
         }
 
         private readonly BeatmapSearchTextBox textBox;
+        private readonly BeatmapSearchMultipleSelectionFilterRow<SearchGeneral> generalFilter;
         private readonly BeatmapSearchRulesetFilterRow modeFilter;
         private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter;
         private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter;
@@ -123,6 +126,7 @@ namespace osu.Game.Overlays.BeatmapListing
                                 Padding = new MarginPadding { Horizontal = 10 },
                                 Children = new Drawable[]
                                 {
+                                    generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(@"General"),
                                     modeFilter = new BeatmapSearchRulesetFilterRow(),
                                     categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"),
                                     genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"),
diff --git a/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs
new file mode 100644
index 0000000000..175942c626
--- /dev/null
+++ b/osu.Game/Overlays/BeatmapListing/SearchGeneral.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Overlays.BeatmapListing
+{
+    public enum SearchGeneral
+    {
+        [Description("Recommended difficulty")]
+        Recommended,
+
+        [Description("Include converted beatmaps")]
+        Converts,
+
+        [Description("Subscribed mappers")]
+        Follows
+    }
+}

From 558f8fa51e2480b0db48dfa5285541992b7c5414 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Max=20H=C3=BCbner?= <maxshuebner@gmail.com>
Date: Thu, 25 Mar 2021 23:20:26 +0100
Subject: [PATCH 407/434] Adjust testscene

---
 .../UserInterface/TestSceneBeatmapListingSearchControl.cs      | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index 9602758ffc..abd1baf0ac 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -34,6 +34,7 @@ namespace osu.Game.Tests.Visual.UserInterface
         public void SetUp() => Schedule(() =>
         {
             OsuSpriteText query;
+            OsuSpriteText general;
             OsuSpriteText ruleset;
             OsuSpriteText category;
             OsuSpriteText genre;
@@ -58,6 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface
                     Children = new Drawable[]
                     {
                         query = new OsuSpriteText(),
+                        general = new OsuSpriteText(),
                         ruleset = new OsuSpriteText(),
                         category = new OsuSpriteText(),
                         genre = new OsuSpriteText(),
@@ -71,6 +73,7 @@ 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().ToLowerInvariant())) : "")}", 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);

From 6a7f9261684602ab3b4633839a1bea1e0acd035c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 13:06:30 +0900
Subject: [PATCH 408/434] Change rounding to use a more general flooring
 approach

---
 osu.Game.Tests/NonVisual/FormatUtilsTest.cs | 2 +-
 osu.Game/Utils/FormatUtils.cs               | 9 ++++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
index eb738f5f67..46d8f4fec4 100644
--- a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
+++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Tests.NonVisual
         [TestCase(0, "0.00%")]
         [TestCase(0.01, "1.00%")]
         [TestCase(0.9899, "98.99%")]
-        [TestCase(0.989999, "99.00%")]
+        [TestCase(0.989999, "98.99%")]
         [TestCase(0.99, "99.00%")]
         [TestCase(0.9999, "99.99%")]
         [TestCase(0.999999, "99.99%")]
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index 4c7702f04e..8312b9e756 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using Humanizer;
 
 namespace osu.Game.Utils
@@ -14,9 +15,11 @@ namespace osu.Game.Utils
         /// <returns>formatted accuracy in percentage</returns>
         public static string FormatAccuracy(this double accuracy)
         {
-            // we don't ever want to show 100% when the accuracy is below perfect, even if it rounds to 100%.
-            if (accuracy < 1 && accuracy > 0.9999)
-                accuracy = 0.9999;
+            // for the sake of display purposes, we don't want to show a user a "rounded up" percentage to the next whole number.
+            // ie. a score which gets 89.99999% shouldn't ever show as 90%.
+            // the reasoning for this is that cutoffs for grade increases are at whole numbers and displaying the required
+            // percentile with a non-matching grade is confusing.
+            accuracy = Math.Floor(accuracy * 10000) / 10000;
 
             return $"{accuracy:0.00%}";
         }

From 4909eaf890715b10a6728270673a7c629f4a7493 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 13:09:53 +0900
Subject: [PATCH 409/434] Add the ability to specify format provider (to make
 tests culture invariant)

---
 osu.Game.Tests/NonVisual/FormatUtilsTest.cs | 3 ++-
 osu.Game/Utils/FormatUtils.cs               | 8 +++++---
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
index 46d8f4fec4..df095ddee3 100644
--- a/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
+++ b/osu.Game.Tests/NonVisual/FormatUtilsTest.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Globalization;
 using NUnit.Framework;
 using osu.Game.Utils;
 
@@ -19,7 +20,7 @@ namespace osu.Game.Tests.NonVisual
         [TestCase(1, "100.00%")]
         public void TestAccuracyFormatting(double input, string expectedOutput)
         {
-            Assert.AreEqual(expectedOutput, input.FormatAccuracy());
+            Assert.AreEqual(expectedOutput, input.FormatAccuracy(CultureInfo.InvariantCulture));
         }
     }
 }
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index 8312b9e756..df1b6cf00d 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Globalization;
 using Humanizer;
 
 namespace osu.Game.Utils
@@ -11,9 +12,10 @@ namespace osu.Game.Utils
         /// <summary>
         /// Turns the provided accuracy into a percentage with 2 decimal places.
         /// </summary>
-        /// <param name="accuracy">The accuracy to be formatted</param>
+        /// <param name="accuracy">The accuracy to be formatted.</param>
+        /// <param name="formatProvider">An optional format provider.</param>
         /// <returns>formatted accuracy in percentage</returns>
-        public static string FormatAccuracy(this double accuracy)
+        public static string FormatAccuracy(this double accuracy, IFormatProvider formatProvider = null)
         {
             // for the sake of display purposes, we don't want to show a user a "rounded up" percentage to the next whole number.
             // ie. a score which gets 89.99999% shouldn't ever show as 90%.
@@ -21,7 +23,7 @@ namespace osu.Game.Utils
             // percentile with a non-matching grade is confusing.
             accuracy = Math.Floor(accuracy * 10000) / 10000;
 
-            return $"{accuracy:0.00%}";
+            return accuracy.ToString("0.00%", formatProvider ?? CultureInfo.CurrentCulture);
         }
 
         /// <summary>

From 17a0b19ee71c09f52baafabdadec7bd71aaafee8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 14:04:09 +0900
Subject: [PATCH 410/434] Always set progress before updating download state

Without doing so, a state change may read from an old progress value.
---
 osu.Game/Online/DownloadTrackingComposite.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs
index 52042c266b..d9599481e7 100644
--- a/osu.Game/Online/DownloadTrackingComposite.cs
+++ b/osu.Game/Online/DownloadTrackingComposite.cs
@@ -123,13 +123,13 @@ namespace osu.Game.Online
             {
                 if (attachedRequest.Progress == 1)
                 {
-                    State.Value = DownloadState.Importing;
                     Progress.Value = 1;
+                    State.Value = DownloadState.Importing;
                 }
                 else
                 {
-                    State.Value = DownloadState.Downloading;
                     Progress.Value = attachedRequest.Progress;
+                    State.Value = DownloadState.Downloading;
 
                     attachedRequest.Failure += onRequestFailure;
                     attachedRequest.DownloadProgressed += onRequestProgress;

From 0a889fafc4fe0df932aeca07939cf4a6cc34c1f6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 14:04:39 +0900
Subject: [PATCH 411/434] Don't send progress updates when not in a downloading
 state

This is mostly just a sanity/debounce check.
---
 osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
index d6f4c45a75..8278162353 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Online.Rooms
 
             Progress.BindValueChanged(_ =>
             {
+                if (State.Value != DownloadState.Downloading)
+                    return;
+
                 // incoming progress changes are going to be at a very high rate.
                 // we don't want to flood the network with this, so rate limit how often we send progress updates.
                 if (progressUpdate?.Completed != false)

From 7faca766e45b0d369326a9bdc26422bb065c825c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 14:49:02 +0900
Subject: [PATCH 412/434] Fix crash on attempting to join lobby using key press
 during a pending join

Closes https://github.com/ppy/osu/issues/12040.
---
 osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index f13d623eae..5a28d0f968 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -170,7 +170,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
 
         private void joinRequested(Room room)
         {
-            Debug.Assert(joiningRoomOperation == null);
+            if (joiningRoomOperation != null)
+                return;
+
             joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
 
             RoomManager?.JoinRoom(room, r =>

From 6a4157d1939f513d0ed1167192b9b7007e8db1d6 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 16:13:43 +0900
Subject: [PATCH 413/434] Remove unused using statement

---
 osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 5a28d0f968..f24577a8a5 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
-using System.Diagnostics;
 using System.Linq;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;

From 2bea69456e8bfbcdf780bd6e8adab5002913f3e3 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Fri, 26 Mar 2021 15:24:33 +0800
Subject: [PATCH 414/434] remove implementations

---
 .../Objects/CatchHitObject.cs                 |  2 --
 .../Objects/ManiaHitObject.cs                 |  2 --
 osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs |  2 --
 osu.Game/Rulesets/Objects/HitObject.cs        |  2 --
 osu.Game/Screens/Edit/Editor.cs               | 23 +++----------------
 5 files changed, 3 insertions(+), 28 deletions(-)

diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 631b50d686..ae45182960 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -120,7 +120,5 @@ namespace osu.Game.Rulesets.Catch.Objects
         }
 
         protected override HitWindows CreateHitWindows() => HitWindows.Empty;
-
-        public override string ToEditorString() => (IndexInCurrentCombo + 1).ToString();
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index c43d223335..27bf50493d 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -22,8 +22,6 @@ namespace osu.Game.Rulesets.Mania.Objects
 
         protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
 
-        public override string ToEditorString() => $"{StartTime}|{Column}";
-
         #region LegacyBeatmapEncoder
 
         float IHasXPosition.X => Column;
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index e784d13084..22b64af3df 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -130,7 +130,5 @@ namespace osu.Game.Rulesets.Osu.Objects
         }
 
         protected override HitWindows CreateHitWindows() => new OsuHitWindows();
-
-        public override string ToEditorString() => (IndexInCurrentCombo + 1).ToString();
     }
 }
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index fa7b2811cc..826d411822 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -168,8 +168,6 @@ namespace osu.Game.Rulesets.Objects
         /// </summary>
         [NotNull]
         protected virtual HitWindows CreateHitWindows() => new HitWindows();
-
-        public virtual string ToEditorString() => string.Empty;
     }
 
     public static class HitObjectExtensions
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index a6e84d59a7..c2a9fd49b1 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -63,9 +63,6 @@ namespace osu.Game.Screens.Edit
 
         protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash;
 
-        [Resolved]
-        private GameHost host { get; set; }
-
         [Resolved]
         private BeatmapManager beatmapManager { get; set; }
 
@@ -110,7 +107,7 @@ namespace osu.Game.Screens.Edit
         private MusicController music { get; set; }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, OsuConfigManager config)
+        private void load(OsuColour colours, GameHost host, OsuConfigManager config)
         {
             if (Beatmap.Value is DummyWorkingBeatmap)
             {
@@ -547,24 +544,10 @@ namespace osu.Game.Screens.Edit
 
         protected void Copy()
         {
-            var builder = new StringBuilder();
-            const string suffix = " - ";
-
             if (editorBeatmap.SelectedHitObjects.Count == 0)
-            {
-                builder.Append(clock.CurrentTime.ToEditorFormattedString());
-            }
-            else
-            {
-                var orderedHitObjects = editorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime);
-                builder.Append(orderedHitObjects.FirstOrDefault().StartTime.ToEditorFormattedString());
-                builder.Append($" ({string.Join(',', orderedHitObjects.Select(h => h.ToEditorString()))})");
+                return;
 
-                clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
-            }
-
-            builder.Append(suffix);
-            host.GetClipboard()?.SetText(builder.ToString());
+            clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
         }
 
         protected void Paste()

From b8b7eb4c4b8e0af156ebdad46a83ecbe3427b0b2 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Fri, 26 Mar 2021 15:25:20 +0800
Subject: [PATCH 415/434] refactor logic to its own component and handle hit
 object to string conversion to its ruleset-specific composers

---
 .../Edit/ManiaHitObjectComposer.cs            |  3 ++
 .../Edit/OsuHitObjectComposer.cs              |  3 ++
 osu.Game/Rulesets/Edit/HitObjectComposer.cs   |  2 +
 .../Screens/Edit/Compose/ComposeScreen.cs     | 23 +++++++++-
 osu.Game/Screens/Edit/SelectionHelper.cs      | 46 +++++++++++++++++++
 5 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 osu.Game/Screens/Edit/SelectionHelper.cs

diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 324670c4b2..4cb34a217c 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -119,5 +119,8 @@ namespace osu.Game.Rulesets.Mania.Edit
                     beatSnapGrid.SelectionTimeRange = null;
             }
         }
+
+        public override IEnumerable<string> ConvertSelectionToString()
+            => EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().Select(h => $"{h.StartTime}|{h.Column}");
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 0490e8b8ce..1943f52c73 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Edit
         protected override ComposeBlueprintContainer CreateBlueprintContainer()
             => new OsuBlueprintContainer(this);
 
+        public override IEnumerable<string> ConvertSelectionToString()
+            => selectedHitObjects.Cast<OsuHitObject>().Select(h => (h.IndexInCurrentCombo + 1).ToString());
+
         private DistanceSnapGrid distanceSnapGrid;
         private Container distanceSnapGridContainer;
 
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index e927951d0a..eee16043e4 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -438,6 +438,8 @@ namespace osu.Game.Rulesets.Edit
         /// </summary>
         public abstract bool CursorInPlacementArea { get; }
 
+        public virtual IEnumerable<string> ConvertSelectionToString() => Array.Empty<string>();
+
         #region IPositionSnapProvider
 
         public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 81b1195a40..c63ef11c74 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -6,6 +6,8 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Edit;
@@ -14,16 +16,19 @@ using osu.Game.Skinning;
 
 namespace osu.Game.Screens.Edit.Compose
 {
-    public class ComposeScreen : EditorScreenWithTimeline
+    public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler<PlatformAction>
     {
         [Resolved]
         private IBindable<WorkingBeatmap> beatmap { get; set; }
 
         private HitObjectComposer composer;
 
+        private SelectionHelper helper;
+
         public ComposeScreen()
             : base(EditorScreenMode.Compose)
         {
+            Add(helper = new SelectionHelper());
         }
 
         private Ruleset ruleset;
@@ -72,5 +77,21 @@ namespace osu.Game.Screens.Edit.Compose
             // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
             return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
         }
+
+        public bool OnPressed(PlatformAction action)
+        {
+            switch (action.ActionType)
+            {
+                case PlatformActionType.Copy:
+                    helper.CopySelectionToClipboard();
+                    return false;
+                default:
+                    return false;
+            };
+        }
+
+        public void OnReleased(PlatformAction action)
+        {
+        }
     }
 }
diff --git a/osu.Game/Screens/Edit/SelectionHelper.cs b/osu.Game/Screens/Edit/SelectionHelper.cs
new file mode 100644
index 0000000000..e0eb868c9f
--- /dev/null
+++ b/osu.Game/Screens/Edit/SelectionHelper.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+using System.Text;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game.Extensions;
+using osu.Game.Rulesets.Edit;
+
+namespace osu.Game.Screens.Edit
+{
+    public class SelectionHelper : Component
+    {
+        [Resolved]
+        private GameHost host { get; set; }
+
+        [Resolved]
+        private EditorClock clock { get; set; }
+
+        [Resolved]
+        private EditorBeatmap editorBeatmap { get; set; }
+
+        [Resolved(CanBeNull = true)]
+        private HitObjectComposer composer { get; set; }
+
+        public void CopySelectionToClipboard()
+        {
+            host.GetClipboard().SetText(formatSelectionAsString());
+        }
+
+        private string formatSelectionAsString()
+        {
+            const string separator = " - ";
+            var builder = new StringBuilder();
+
+            if (!editorBeatmap.SelectedHitObjects.Any())
+            {
+                builder.Append($"{clock.CurrentTime.ToEditorFormattedString()}{separator}");
+                return builder.ToString();
+            };
+
+            builder.Append(editorBeatmap.SelectedHitObjects.First().StartTime.ToEditorFormattedString());
+            builder.Append($" ({string.Join(',', composer.ConvertSelectionToString())}){separator}");
+            return builder.ToString();
+        }
+    }
+}

From cb48e5f15822a7aab53317bc8a1b938c777f8544 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 26 Mar 2021 16:33:16 +0900
Subject: [PATCH 416/434] Fix timeline not visually ordering hitobjects in a
 stable way

---
 .../Components/HitObjectOrderedSelectionContainer.cs      | 8 +++++++-
 .../Components/Timeline/TimelineBlueprintContainer.cs     | 2 +-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
index 9e95fe4fa1..d612cf3fe0 100644
--- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs
@@ -71,7 +71,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
 
             // Put earlier blueprints towards the end of the list, so they handle input first
             int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
-            return i == 0 ? CompareReverseChildID(x, y) : i;
+
+            if (i != 0) return i;
+
+            // Fall back to end time if the start time is equal.
+            i = yObj.HitObject.GetEndTime().CompareTo(xObj.HitObject.GetEndTime());
+
+            return i == 0 ? CompareReverseChildID(y, x) : i;
         }
     }
 }
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index cec851b5bb..3623f8ad8e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
 
             Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
 
-            foreach (var b in SelectionBlueprints.OrderBy(b => b.HitObject.StartTime).ThenBy(b => b.HitObject.GetEndTime()))
+            foreach (var b in SelectionBlueprints.Reverse())
             {
                 // remove objects from the stack as long as their end time is in the past.
                 while (currentConcurrentObjects.TryPeek(out HitObject hitObject))

From 374f8c5e229b314a2f7cf1de9b29285cccbff0d2 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Fri, 26 Mar 2021 15:33:28 +0800
Subject: [PATCH 417/434] move to compose namespace and add license header

---
 osu.Game/Screens/Edit/{ => Compose}/SelectionHelper.cs | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
 rename osu.Game/Screens/Edit/{ => Compose}/SelectionHelper.cs (85%)

diff --git a/osu.Game/Screens/Edit/SelectionHelper.cs b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
similarity index 85%
rename from osu.Game/Screens/Edit/SelectionHelper.cs
rename to osu.Game/Screens/Edit/Compose/SelectionHelper.cs
index e0eb868c9f..39d6d57a2a 100644
--- a/osu.Game/Screens/Edit/SelectionHelper.cs
+++ b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
@@ -1,4 +1,7 @@
-using System.Linq;
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
 using System.Text;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
@@ -6,7 +9,7 @@ using osu.Framework.Platform;
 using osu.Game.Extensions;
 using osu.Game.Rulesets.Edit;
 
-namespace osu.Game.Screens.Edit
+namespace osu.Game.Screens.Edit.Compose
 {
     public class SelectionHelper : Component
     {

From 71a0616861916129456e0a301d0386269c51cd6c Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Fri, 26 Mar 2021 15:34:45 +0800
Subject: [PATCH 418/434] remove extra semi colons

---
 osu.Game/Screens/Edit/Compose/ComposeScreen.cs   | 2 +-
 osu.Game/Screens/Edit/Compose/SelectionHelper.cs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index c63ef11c74..b249ae9bcd 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.Edit.Compose
                     return false;
                 default:
                     return false;
-            };
+            }
         }
 
         public void OnReleased(PlatformAction action)
diff --git a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
index 39d6d57a2a..2e172c12dc 100644
--- a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
+++ b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Compose
             {
                 builder.Append($"{clock.CurrentTime.ToEditorFormattedString()}{separator}");
                 return builder.ToString();
-            };
+            }
 
             builder.Append(editorBeatmap.SelectedHitObjects.First().StartTime.ToEditorFormattedString());
             builder.Append($" ({string.Join(',', composer.ConvertSelectionToString())}){separator}");

From 1b6e08b3eb34f80afc5126c2af1efb7ba16d8d19 Mon Sep 17 00:00:00 2001
From: Salman Ahmed <frenzibyte@gmail.com>
Date: Fri, 26 Mar 2021 10:35:46 +0300
Subject: [PATCH 419/434] Fix incorrect default font overlap for score and
 combo

---
 osu.Game/Skinning/LegacySkinExtensions.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index d08f50bccb..d8fb1fa664 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -97,10 +97,10 @@ namespace osu.Game.Skinning
             switch (font)
             {
                 case LegacyFont.Score:
-                    return source.GetConfig<LegacySetting, float>(LegacySetting.ScoreOverlap)?.Value ?? -2f;
+                    return source.GetConfig<LegacySetting, float>(LegacySetting.ScoreOverlap)?.Value ?? 0f;
 
                 case LegacyFont.Combo:
-                    return source.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;
+                    return source.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? 0f;
 
                 case LegacyFont.HitCircle:
                     return source.GetConfig<LegacySetting, float>(LegacySetting.HitCircleOverlap)?.Value ?? -2f;

From c96321206a0e5157d8864e5d84204706f918ce30 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Fri, 26 Mar 2021 16:17:24 +0800
Subject: [PATCH 420/434] fix appveyor complaints

---
 osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 3 ++-
 osu.Game/Screens/Edit/Editor.cs                | 6 +-----
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index b249ae9bcd..16043ff64b 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Compose
 
         private HitObjectComposer composer;
 
-        private SelectionHelper helper;
+        private readonly SelectionHelper helper;
 
         public ComposeScreen()
             : base(EditorScreenMode.Compose)
@@ -85,6 +85,7 @@ namespace osu.Game.Screens.Edit.Compose
                 case PlatformActionType.Copy:
                     helper.CopySelectionToClipboard();
                     return false;
+
                 default:
                     return false;
             }
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index c2a9fd49b1..d9ba12d331 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -5,7 +5,6 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
-using System.Text;
 using osu.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -17,12 +16,10 @@ using osu.Framework.Input;
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Events;
 using osu.Framework.Logging;
-using osu.Framework.Platform;
 using osu.Framework.Screens;
 using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
-using osu.Game.Extensions;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Cursor;
 using osu.Game.Graphics.UserInterface;
@@ -31,7 +28,6 @@ using osu.Game.IO.Serialization;
 using osu.Game.Online.API;
 using osu.Game.Overlays;
 using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Screens.Edit.Components;
 using osu.Game.Screens.Edit.Components.Menus;
 using osu.Game.Screens.Edit.Components.Timelines.Summary;
@@ -107,7 +103,7 @@ namespace osu.Game.Screens.Edit
         private MusicController music { get; set; }
 
         [BackgroundDependencyLoader]
-        private void load(OsuColour colours, GameHost host, OsuConfigManager config)
+        private void load(OsuColour colours, OsuConfigManager config)
         {
             if (Beatmap.Value is DummyWorkingBeatmap)
             {

From 21398e25b5b939ae40e15be42bb6ffb93365d315 Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Sat, 27 Mar 2021 10:02:21 +0800
Subject: [PATCH 421/434] null check composer and ensure the correct start time
 from selected hit objects

---
 osu.Game/Screens/Edit/Compose/SelectionHelper.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
index 2e172c12dc..a01a9e0144 100644
--- a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
+++ b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
@@ -41,8 +41,10 @@ namespace osu.Game.Screens.Edit.Compose
                 return builder.ToString();
             }
 
-            builder.Append(editorBeatmap.SelectedHitObjects.First().StartTime.ToEditorFormattedString());
-            builder.Append($" ({string.Join(',', composer.ConvertSelectionToString())}){separator}");
+            string hitObjects = composer != null ? string.Join(',', composer.ConvertSelectionToString()) : string.Empty;
+
+            builder.Append(editorBeatmap.SelectedHitObjects.Min(h => h.StartTime).ToEditorFormattedString());
+            builder.Append($" ({hitObjects}){separator}");
             return builder.ToString();
         }
     }

From 699a317b445ff557163d91143682ab6f6c5faf8d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 18:07:47 +0900
Subject: [PATCH 422/434] Fix chat scroll sticking visually when scrolling
 beyond bottom extent

---
 osu.Game/Overlays/Chat/DrawableChannel.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index 86ce724390..5f9c00b36a 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -275,7 +275,8 @@ namespace osu.Game.Overlays.Chat
                     {
                         if (!UserScrolling)
                         {
-                            ScrollToEnd();
+                            if (Current < ScrollableExtent)
+                                ScrollToEnd();
                             lastExtent = ScrollableExtent;
                         }
                     });

From 9a02f3868c2105e51dbd7e64ce4443d08938d02e Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Mon, 29 Mar 2021 17:29:05 +0800
Subject: [PATCH 423/434] return a string instead

---
 osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 ++--
 osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs     | 4 ++--
 osu.Game/Rulesets/Edit/HitObjectComposer.cs            | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 4cb34a217c..d9570bf8be 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Edit
             }
         }
 
-        public override IEnumerable<string> ConvertSelectionToString()
-            => EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().Select(h => $"{h.StartTime}|{h.Column}");
+        public override string ConvertSelectionToString()
+            => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 1943f52c73..396fd41377 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -82,8 +82,8 @@ namespace osu.Game.Rulesets.Osu.Edit
         protected override ComposeBlueprintContainer CreateBlueprintContainer()
             => new OsuBlueprintContainer(this);
 
-        public override IEnumerable<string> ConvertSelectionToString()
-            => selectedHitObjects.Cast<OsuHitObject>().Select(h => (h.IndexInCurrentCombo + 1).ToString());
+        public override string ConvertSelectionToString()
+            => string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
 
         private DistanceSnapGrid distanceSnapGrid;
         private Container distanceSnapGridContainer;
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index eee16043e4..736fc47dee 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -438,7 +438,7 @@ namespace osu.Game.Rulesets.Edit
         /// </summary>
         public abstract bool CursorInPlacementArea { get; }
 
-        public virtual IEnumerable<string> ConvertSelectionToString() => Array.Empty<string>();
+        public virtual string ConvertSelectionToString() => string.Empty;
 
         #region IPositionSnapProvider
 

From cdb779f764bb09dcba1784f66fc87aab9debfd8b Mon Sep 17 00:00:00 2001
From: Nathan Alo <n20gaming2000@gmail.com>
Date: Mon, 29 Mar 2021 17:30:23 +0800
Subject: [PATCH 424/434] move copy logic inside ComposeScreen

---
 .../Screens/Edit/Compose/ComposeScreen.cs     | 37 ++++++++++----
 .../Screens/Edit/Compose/SelectionHelper.cs   | 51 -------------------
 2 files changed, 26 insertions(+), 62 deletions(-)
 delete mode 100644 osu.Game/Screens/Edit/Compose/SelectionHelper.cs

diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 16043ff64b..f6ce5a4e3d 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -2,13 +2,17 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Diagnostics;
+using System.Linq;
+using System.Text;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input;
 using osu.Framework.Input.Bindings;
+using osu.Framework.Platform;
 using osu.Game.Beatmaps;
+using osu.Game.Extensions;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Screens.Edit.Compose.Components.Timeline;
@@ -21,14 +25,17 @@ namespace osu.Game.Screens.Edit.Compose
         [Resolved]
         private IBindable<WorkingBeatmap> beatmap { get; set; }
 
-        private HitObjectComposer composer;
+        [Resolved]
+        private GameHost host { get; set; }
 
-        private readonly SelectionHelper helper;
+        [Resolved]
+        private EditorClock clock { get; set; }
+
+        private HitObjectComposer composer;
 
         public ComposeScreen()
             : base(EditorScreenMode.Compose)
         {
-            Add(helper = new SelectionHelper());
         }
 
         private Ruleset ruleset;
@@ -78,17 +85,25 @@ namespace osu.Game.Screens.Edit.Compose
             return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
         }
 
+        private string formatSelectionAsString()
+        {
+            var builder = new StringBuilder();
+            builder.Append(EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).FirstOrDefault()?.StartTime.ToEditorFormattedString() ?? clock.CurrentTime.ToEditorFormattedString());
+
+            if (EditorBeatmap.SelectedHitObjects.Any() && composer != null)
+                builder.Append($" ({composer.ConvertSelectionToString()})");
+
+            builder.Append(" - ");
+
+            return builder.ToString();
+        }
+
         public bool OnPressed(PlatformAction action)
         {
-            switch (action.ActionType)
-            {
-                case PlatformActionType.Copy:
-                    helper.CopySelectionToClipboard();
-                    return false;
+            if (action.ActionType == PlatformActionType.Copy)
+                host.GetClipboard().SetText(formatSelectionAsString());
 
-                default:
-                    return false;
-            }
+            return false;
         }
 
         public void OnReleased(PlatformAction action)
diff --git a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs b/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
deleted file mode 100644
index a01a9e0144..0000000000
--- a/osu.Game/Screens/Edit/Compose/SelectionHelper.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using System.Text;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Platform;
-using osu.Game.Extensions;
-using osu.Game.Rulesets.Edit;
-
-namespace osu.Game.Screens.Edit.Compose
-{
-    public class SelectionHelper : Component
-    {
-        [Resolved]
-        private GameHost host { get; set; }
-
-        [Resolved]
-        private EditorClock clock { get; set; }
-
-        [Resolved]
-        private EditorBeatmap editorBeatmap { get; set; }
-
-        [Resolved(CanBeNull = true)]
-        private HitObjectComposer composer { get; set; }
-
-        public void CopySelectionToClipboard()
-        {
-            host.GetClipboard().SetText(formatSelectionAsString());
-        }
-
-        private string formatSelectionAsString()
-        {
-            const string separator = " - ";
-            var builder = new StringBuilder();
-
-            if (!editorBeatmap.SelectedHitObjects.Any())
-            {
-                builder.Append($"{clock.CurrentTime.ToEditorFormattedString()}{separator}");
-                return builder.ToString();
-            }
-
-            string hitObjects = composer != null ? string.Join(',', composer.ConvertSelectionToString()) : string.Empty;
-
-            builder.Append(editorBeatmap.SelectedHitObjects.Min(h => h.StartTime).ToEditorFormattedString());
-            builder.Append($" ({hitObjects}){separator}");
-            return builder.ToString();
-        }
-    }
-}

From 90ab765cf519be8be956175f492f3c7aff4e0f0e Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 18:46:32 +0900
Subject: [PATCH 425/434] Reorder methods and surround with region

---
 .../Screens/Edit/Compose/ComposeScreen.cs     | 26 +++++++++++--------
 1 file changed, 15 insertions(+), 11 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index f6ce5a4e3d..1d52cc60fd 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -85,6 +85,20 @@ namespace osu.Game.Screens.Edit.Compose
             return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content));
         }
 
+        #region Input Handling
+
+        public bool OnPressed(PlatformAction action)
+        {
+            if (action.ActionType == PlatformActionType.Copy)
+                host.GetClipboard().SetText(formatSelectionAsString());
+
+            return false;
+        }
+
+        public void OnReleased(PlatformAction action)
+        {
+        }
+
         private string formatSelectionAsString()
         {
             var builder = new StringBuilder();
@@ -98,16 +112,6 @@ namespace osu.Game.Screens.Edit.Compose
             return builder.ToString();
         }
 
-        public bool OnPressed(PlatformAction action)
-        {
-            if (action.ActionType == PlatformActionType.Copy)
-                host.GetClipboard().SetText(formatSelectionAsString());
-
-            return false;
-        }
-
-        public void OnReleased(PlatformAction action)
-        {
-        }
+        #endregion
     }
 }

From 3909eda095ce5f6fcfdc7afa7e3ea4fdaf826540 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 18:51:28 +0900
Subject: [PATCH 426/434] Avoid using a StringBuilder

---
 osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 1d52cc60fd..61056aeced 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -3,7 +3,6 @@
 
 using System.Diagnostics;
 using System.Linq;
-using System.Text;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -101,15 +100,15 @@ namespace osu.Game.Screens.Edit.Compose
 
         private string formatSelectionAsString()
         {
-            var builder = new StringBuilder();
-            builder.Append(EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).FirstOrDefault()?.StartTime.ToEditorFormattedString() ?? clock.CurrentTime.ToEditorFormattedString());
+            if (composer == null)
+                return string.Empty;
 
-            if (EditorBeatmap.SelectedHitObjects.Any() && composer != null)
-                builder.Append($" ({composer.ConvertSelectionToString()})");
+            double displayTime = EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).FirstOrDefault()?.StartTime ?? clock.CurrentTime;
+            string selectionAsString = composer.ConvertSelectionToString();
 
-            builder.Append(" - ");
-
-            return builder.ToString();
+            return !string.IsNullOrEmpty(selectionAsString)
+                ? $"{displayTime.ToEditorFormattedString()} ({selectionAsString}) - "
+                : $"{displayTime.ToEditorFormattedString()} - ";
         }
 
         #endregion

From badf5ee4a2403998b4508907425630b21ea9e392 Mon Sep 17 00:00:00 2001
From: Shivam <s.r.v.ramkisoen@umail.leidenuniv.nl>
Date: Mon, 29 Mar 2021 15:03:10 +0200
Subject: [PATCH 427/434] Fix stable.json file directory location due to the
 change of how TournamentStorage works

---
 .../NonVisual/IPCLocationTest.cs              | 68 +++++++++++++++++++
 osu.Game.Tournament/IO/TournamentStorage.cs   | 21 +++---
 osu.Game.Tournament/Models/StableInfo.cs      |  6 +-
 3 files changed, 85 insertions(+), 10 deletions(-)
 create mode 100644 osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs

diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
new file mode 100644
index 0000000000..29586dfdec
--- /dev/null
+++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
@@ -0,0 +1,68 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Platform;
+using osu.Game.Tournament.IO;
+using osu.Game.Tournament.IPC;
+
+namespace osu.Game.Tournament.Tests.NonVisual
+{
+    [TestFixture]
+    public class IPCLocationTest
+    {
+        [Test]
+        public void CheckIPCLocation()
+        {
+            // don't use clean run because files are being written before osu! launches.
+            using (HeadlessGameHost host = new HeadlessGameHost(nameof(CheckIPCLocation)))
+            {
+                string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(CheckIPCLocation));
+
+                // Set up a fake IPC client for the IPC Storage to switch to.
+                string testCeDir = Path.Combine(basePath, "stable-ce");
+                Directory.CreateDirectory(testCeDir);
+
+                string ipcFile = Path.Combine(testCeDir, "ipc.txt");
+                File.WriteAllText(ipcFile, string.Empty);
+
+                try
+                {
+                    var osu = loadOsu(host);
+                    TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>();
+                    FileBasedIPC ipc = (FileBasedIPC)osu.Dependencies.Get<MatchIPCInfo>();
+
+                    Assert.True(ipc.SetIPCLocation(testCeDir));
+                    Assert.True(storage.AllTournaments.Exists("stable.json"));
+                }
+                finally
+                {
+                    host.Storage.DeleteDirectory(testCeDir);
+                    host.Storage.DeleteDirectory("tournaments");
+                    host.Exit();
+                }
+            }
+        }
+
+        private TournamentGameBase loadOsu(GameHost host)
+        {
+            var osu = new TournamentGameBase();
+            Task.Run(() => host.Run(osu));
+            waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
+            return osu;
+        }
+
+        private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
+        {
+            Task task = Task.Run(() =>
+            {
+                while (!result()) Thread.Sleep(200);
+            });
+
+            Assert.IsTrue(task.Wait(timeout), failureMessage);
+        }
+    }
+}
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
index 5d9fed6288..044b60bbd5 100644
--- a/osu.Game.Tournament/IO/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentStorage.cs
@@ -1,12 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Collections.Generic;
+using System.IO;
 using osu.Framework.Bindables;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.IO;
-using System.IO;
-using System.Collections.Generic;
 using osu.Game.Tournament.Configuration;
 
 namespace osu.Game.Tournament.IO
@@ -15,7 +15,12 @@ namespace osu.Game.Tournament.IO
     {
         private const string default_tournament = "default";
         private readonly Storage storage;
-        private readonly Storage allTournaments;
+
+        /// <summary>
+        /// The storage where all tournaments are located.
+        /// </summary>
+        public readonly Storage AllTournaments;
+
         private readonly TournamentStorageManager storageConfig;
         public readonly Bindable<string> CurrentTournament;
 
@@ -23,16 +28,16 @@ namespace osu.Game.Tournament.IO
             : base(storage.GetStorageForDirectory("tournaments"), string.Empty)
         {
             this.storage = storage;
-            allTournaments = UnderlyingStorage;
+            AllTournaments = UnderlyingStorage;
 
             storageConfig = new TournamentStorageManager(storage);
 
             if (storage.Exists("tournament.ini"))
             {
-                ChangeTargetStorage(allTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
+                ChangeTargetStorage(AllTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
             }
             else
-                Migrate(allTournaments.GetStorageForDirectory(default_tournament));
+                Migrate(AllTournaments.GetStorageForDirectory(default_tournament));
 
             CurrentTournament = storageConfig.GetBindable<string>(StorageConfig.CurrentTournament);
             Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
@@ -42,11 +47,11 @@ namespace osu.Game.Tournament.IO
 
         private void updateTournament(ValueChangedEvent<string> newTournament)
         {
-            ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue));
+            ChangeTargetStorage(AllTournaments.GetStorageForDirectory(newTournament.NewValue));
             Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
         }
 
-        public IEnumerable<string> ListTournaments() => allTournaments.GetDirectories(string.Empty);
+        public IEnumerable<string> ListTournaments() => AllTournaments.GetDirectories(string.Empty);
 
         public override void Migrate(Storage newStorage)
         {
diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs
index 0b0050a245..d390f88d59 100644
--- a/osu.Game.Tournament/Models/StableInfo.cs
+++ b/osu.Game.Tournament/Models/StableInfo.cs
@@ -5,6 +5,7 @@ using System;
 using System.IO;
 using Newtonsoft.Json;
 using osu.Framework.Platform;
+using osu.Game.Tournament.IO;
 
 namespace osu.Game.Tournament.Models
 {
@@ -24,13 +25,14 @@ namespace osu.Game.Tournament.Models
         /// </summary>
         public event Action OnStableInfoSaved;
 
-        private const string config_path = "tournament/stable.json";
+        private const string config_path = "stable.json";
 
         private readonly Storage storage;
 
         public StableInfo(Storage storage)
         {
-            this.storage = storage;
+            TournamentStorage tStorage = (TournamentStorage)storage;
+            this.storage = tStorage.AllTournaments;
 
             if (!storage.Exists(config_path))
                 return;

From 36364a449249fb42289e82e1e8d6c05105a43885 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 22:12:49 +0900
Subject: [PATCH 428/434] Update framework

---
 osu.Android.props                             | 2 +-
 osu.Game/Input/Handlers/ReplayInputHandler.cs | 2 --
 osu.Game/osu.Game.csproj                      | 2 +-
 osu.iOS.props                                 | 4 ++--
 4 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/osu.Android.props b/osu.Android.props
index 75ac298626..3682a44b9f 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.323.0" />
+    <PackageReference Include="ppy.osu.Framework.Android" Version="2021.329.0" />
   </ItemGroup>
 </Project>
diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs
index 93ed3ca884..fba1bee0b8 100644
--- a/osu.Game/Input/Handlers/ReplayInputHandler.cs
+++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Input.Handlers
 
         public override bool IsActive => true;
 
-        public override int Priority => 0;
-
         public class ReplayState<T> : IInput
             where T : struct
         {
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b90c938a8b..35b0827715 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.323.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.329.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
     <PackageReference Include="Sentry" Version="3.0.7" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
diff --git a/osu.iOS.props b/osu.iOS.props
index ce182a3054..ceb46eae87 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
     <Reference Include="System.Net.Http" />
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.323.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.329.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@@ -93,7 +93,7 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
-    <PackageReference Include="ppy.osu.Framework" Version="2021.323.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2021.329.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />

From d84c9251e6d4d6569e5f895a7cf6943c2c722467 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 22:17:24 +0900
Subject: [PATCH 429/434] Update nuget packages

---
 osu.Game/osu.Game.csproj | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 35b0827715..6d571218fc 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,20 +18,20 @@
     </None>
   </ItemGroup>
   <ItemGroup Label="Package References">
-    <PackageReference Include="DiffPlex" Version="1.6.3" />
+    <PackageReference Include="DiffPlex" Version="1.7.0" />
     <PackageReference Include="Humanizer" Version="2.8.26" />
     <PackageReference Include="MessagePack" Version="2.2.85" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.3" />
-    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.2" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.4" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.4" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.4" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
-    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.329.0" />
     <PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
-    <PackageReference Include="Sentry" Version="3.0.7" />
+    <PackageReference Include="Sentry" Version="3.2.0" />
     <PackageReference Include="SharpCompress" Version="0.28.1" />
     <PackageReference Include="NUnit" Version="3.13.1" />
     <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

From 6c5a10a7449a24fdd6932c6cb8ba99c7d98affd6 Mon Sep 17 00:00:00 2001
From: Shivam <s.r.v.ramkisoen@umail.leidenuniv.nl>
Date: Mon, 29 Mar 2021 15:27:25 +0200
Subject: [PATCH 430/434] Add missing license header

---
 osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
index 29586dfdec..086ed54435 100644
--- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
@@ -1,3 +1,6 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
 using System;
 using System.IO;
 using System.Threading;

From 6f01070408fb5fad37d33765a22997d1189d9cc8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 29 Mar 2021 23:06:29 +0900
Subject: [PATCH 431/434] Add weird android package requirements

---
 osu.Android/osu.Android.csproj                               | 5 +++++
 .../osu.Game.Rulesets.Catch.Tests.Android.csproj             | 5 +++++
 .../osu.Game.Rulesets.Mania.Tests.Android.csproj             | 5 +++++
 .../osu.Game.Rulesets.Osu.Tests.Android.csproj               | 5 +++++
 .../osu.Game.Rulesets.Taiko.Tests.Android.csproj             | 5 +++++
 osu.Game.Tests.Android/osu.Game.Tests.Android.csproj         | 3 +++
 6 files changed, 28 insertions(+)

diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index a2638e95c8..2051beae21 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -53,5 +53,10 @@
   <ItemGroup>
     <AndroidResource Include="Resources\drawable\lazer.png" />
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
index 88b420ffad..2e6c10a02e 100644
--- a/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Catch.Tests.Android/osu.Game.Rulesets.Catch.Tests.Android.csproj
@@ -35,5 +35,10 @@
       <Name>osu.Game</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
index 0e557cb260..8c134c7114 100644
--- a/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Mania.Tests.Android/osu.Game.Rulesets.Mania.Tests.Android.csproj
@@ -35,5 +35,10 @@
       <Name>osu.Game</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
index dcf1573522..22fa605176 100644
--- a/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Osu.Tests.Android/osu.Game.Rulesets.Osu.Tests.Android.csproj
@@ -35,5 +35,10 @@
       <Name>osu.Game</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
index 392442b713..a48110b354 100644
--- a/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests.Android/osu.Game.Rulesets.Taiko.Tests.Android.csproj
@@ -35,5 +35,10 @@
       <Name>osu.Game</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>
\ No newline at end of file
diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
index c3d9cb5875..bf256f486c 100644
--- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
+++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj
@@ -75,6 +75,9 @@
   <ItemGroup Label="Package References">
     <PackageReference Include="DeepEqual" Version="2.0.0" />
     <PackageReference Include="Moq" Version="4.16.1" />
+    <PackageReference Include="System.Formats.Asn1">
+      <Version>5.0.0</Version>
+    </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
 </Project>

From 2d344ae6ffd9a5dee8e47096060a1c4f949b677e Mon Sep 17 00:00:00 2001
From: Shivam <s.r.v.ramkisoen@umail.leidenuniv.nl>
Date: Mon, 29 Mar 2021 16:16:50 +0200
Subject: [PATCH 432/434] wait for IPC to be populated in the test

Did not see this when locally running test until after a couple of subsequent runs.
---
 osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
index 086ed54435..4791da93c6 100644
--- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
                     TournamentStorage storage = (TournamentStorage)osu.Dependencies.Get<Storage>();
                     FileBasedIPC ipc = (FileBasedIPC)osu.Dependencies.Get<MatchIPCInfo>();
 
+                    waitForOrAssert(() => ipc != null, @"ipc could not be populated in a reasonable amount of time");
+
                     Assert.True(ipc.SetIPCLocation(testCeDir));
                     Assert.True(storage.AllTournaments.Exists("stable.json"));
                 }

From 804ffe9f48954378ef688bd9d3d58120d0ccbf21 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 30 Mar 2021 09:00:09 +0900
Subject: [PATCH 433/434] Fix inspections

---
 .../Online/TestAPIModJsonSerialization.cs     | 22 +++++++++----------
 .../Screens/Editors/TeamEditorScreen.cs       |  8 +++++--
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index ab24a72a12..77f910c144 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Online
 
             var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
 
-            Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym));
+            Assert.That(deserialized?.Acronym, Is.EqualTo(apiMod.Acronym));
         }
 
         [Test]
@@ -35,7 +35,7 @@ namespace osu.Game.Tests.Online
 
             var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
 
-            Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0));
+            Assert.That(deserialized?.Settings, Contains.Key("test_setting").With.ContainValue(2.0));
         }
 
         [Test]
@@ -44,9 +44,9 @@ namespace osu.Game.Tests.Online
             var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } });
 
             var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
-            var converted = (TestMod)deserialized.ToMod(new TestRuleset());
+            var converted = (TestMod)deserialized?.ToMod(new TestRuleset());
 
-            Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
+            Assert.That(converted?.TestSetting.Value, Is.EqualTo(2));
         }
 
         [Test]
@@ -61,11 +61,11 @@ namespace osu.Game.Tests.Online
             });
 
             var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
-            var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
+            var converted = (TestModTimeRamp)deserialised?.ToMod(new TestRuleset());
 
-            Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
-            Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
-            Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
+            Assert.That(converted?.AdjustPitch.Value, Is.EqualTo(false));
+            Assert.That(converted?.InitialRate.Value, Is.EqualTo(1.25));
+            Assert.That(converted?.FinalRate.Value, Is.EqualTo(0.25));
         }
 
         [Test]
@@ -78,10 +78,10 @@ namespace osu.Game.Tests.Online
             });
 
             var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
-            var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset());
+            var converted = (TestModDifficultyAdjust)deserialised?.ToMod(new TestRuleset());
 
-            Assert.That(converted.ExtendedLimits.Value, Is.True);
-            Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11));
+            Assert.That(converted?.ExtendedLimits.Value, Is.True);
+            Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
         }
 
         private class TestRuleset : Ruleset
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 582f72429b..f051823541 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -43,12 +43,16 @@ namespace osu.Game.Tournament.Screens.Editors
         private void addAllCountries()
         {
             List<TournamentTeam> countries;
+
             using (Stream stream = game.Resources.GetStream("Resources/countries.json"))
             using (var sr = new StreamReader(stream))
                 countries = JsonConvert.DeserializeObject<List<TournamentTeam>>(sr.ReadToEnd());
 
-            foreach (var c in countries)
-                Storage.Add(c);
+            if (countries != null)
+            {
+                foreach (var c in countries)
+                    Storage.Add(c);
+            }
         }
 
         public class TeamRow : CompositeDrawable, IModelBacked<TournamentTeam>

From 69db0a55938176ca9ad4b67cb2922c00d896f934 Mon Sep 17 00:00:00 2001
From: smoogipoo <smoogipoo@smgi.me>
Date: Tue, 30 Mar 2021 09:03:32 +0900
Subject: [PATCH 434/434] Countries should not be null (internal game resource)

---
 .../Screens/Editors/TeamEditorScreen.cs                | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index f051823541..aa1be143ea 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using Newtonsoft.Json;
@@ -48,11 +49,10 @@ namespace osu.Game.Tournament.Screens.Editors
             using (var sr = new StreamReader(stream))
                 countries = JsonConvert.DeserializeObject<List<TournamentTeam>>(sr.ReadToEnd());
 
-            if (countries != null)
-            {
-                foreach (var c in countries)
-                    Storage.Add(c);
-            }
+            Debug.Assert(countries != null);
+
+            foreach (var c in countries)
+                Storage.Add(c);
         }
 
         public class TeamRow : CompositeDrawable, IModelBacked<TournamentTeam>