diff --git a/osu.Android.props b/osu.Android.props
index ecfaff0547..ec223f98c2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
     <Reference Include="Java.Interop" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
     <PackageReference Include="ppy.osu.Framework.Android" Version="2021.813.0" />
   </ItemGroup>
   <ItemGroup Label="Transitive Dependencies">
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index a3307c9224..6abfbdbe21 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
             AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
             AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
+            AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
 
             AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
             AddStep("catch many random fruit", () =>
diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
index e736d68740..371e901c69 100644
--- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
+++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
         Banana,
         Droplet,
         Catcher,
-        CatchComboCounter
+        CatchComboCounter,
+        HitExplosion
     }
 }
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs
new file mode 100644
index 0000000000..e1fad564a3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultHitExplosion.cs
@@ -0,0 +1,129 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Utils;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Default
+{
+    public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
+    {
+        private CircularContainer largeFaint;
+        private CircularContainer smallFaint;
+        private CircularContainer directionalGlow1;
+        private CircularContainer directionalGlow2;
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            Size = new Vector2(20);
+            Anchor = Anchor.BottomCentre;
+            Origin = Anchor.BottomCentre;
+
+            // scale roughly in-line with visual appearance of notes
+            const float initial_height = 10;
+
+            InternalChildren = new Drawable[]
+            {
+                largeFaint = new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Blending = BlendingParameters.Additive,
+                },
+                smallFaint = new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Blending = BlendingParameters.Additive,
+                },
+                directionalGlow1 = new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Size = new Vector2(0.01f, initial_height),
+                    Blending = BlendingParameters.Additive,
+                },
+                directionalGlow2 = new CircularContainer
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    RelativeSizeAxes = Axes.Both,
+                    Masking = true,
+                    Size = new Vector2(0.01f, initial_height),
+                    Blending = BlendingParameters.Additive,
+                }
+            };
+        }
+
+        public void Animate(HitExplosionEntry entry)
+        {
+            X = entry.Position;
+            Scale = new Vector2(entry.HitObject.Scale);
+            setColour(entry.ObjectColour);
+
+            using (BeginAbsoluteSequence(entry.LifetimeStart))
+                applyTransforms(entry.HitObject.RandomSeed);
+        }
+
+        private void applyTransforms(int randomSeed)
+        {
+            const double duration = 400;
+
+            // we want our size to be very small so the glow dominates it.
+            largeFaint.Size = new Vector2(0.8f);
+            largeFaint
+                .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
+                .FadeOut(duration * 2);
+
+            const float angle_variangle = 15; // should be less than 45
+            directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
+            directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
+
+            this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
+        }
+
+        private void setColour(Color4 objectColour)
+        {
+            const float roundness = 100;
+
+            largeFaint.EdgeEffect = new EdgeEffectParameters
+            {
+                Type = EdgeEffectType.Glow,
+                Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+                Roundness = 160,
+                Radius = 200,
+            };
+
+            smallFaint.EdgeEffect = new EdgeEffectParameters
+            {
+                Type = EdgeEffectType.Glow,
+                Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+                Roundness = 20,
+                Radius = 50,
+            };
+
+            directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
+            {
+                Type = EdgeEffectType.Glow,
+                Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
+                Roundness = roundness,
+                Radius = 40,
+            };
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index 5e744ec001..10fc4e78b2 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
 
                         if (version < 2.3m)
                         {
-                            if (GetTexture(@"fruit-ryuuta") != null ||
-                                GetTexture(@"fruit-ryuuta-0") != null)
+                            if (hasOldStyleCatcherSprite())
                                 return new LegacyCatcherOld();
                         }
 
-                        if (GetTexture(@"fruit-catcher-idle") != null ||
-                            GetTexture(@"fruit-catcher-idle-0") != null)
+                        if (hasNewStyleCatcherSprite())
                             return new LegacyCatcherNew();
 
                         return null;
@@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
                             return new LegacyCatchComboCounter(Skin);
 
                         return null;
+
+                    case CatchSkinComponents.HitExplosion:
+                        if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
+                            return new LegacyHitExplosion();
+
+                        return null;
                 }
             }
 
             return base.GetDrawableComponent(component);
         }
 
+        private bool hasOldStyleCatcherSprite() =>
+            GetTexture(@"fruit-ryuuta") != null
+            || GetTexture(@"fruit-ryuuta-0") != null;
+
+        private bool hasNewStyleCatcherSprite() =>
+            GetTexture(@"fruit-catcher-idle") != null
+            || GetTexture(@"fruit-catcher-idle-0") != null;
+
         public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
         {
             switch (lookup)
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
new file mode 100644
index 0000000000..c262b0a4ac
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
@@ -0,0 +1,94 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Legacy
+{
+    public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
+    {
+        [Resolved]
+        private Catcher catcher { get; set; }
+
+        private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2;
+
+        private readonly Sprite explosion1;
+        private readonly Sprite explosion2;
+
+        public LegacyHitExplosion()
+        {
+            Anchor = Anchor.BottomCentre;
+            Origin = Anchor.BottomCentre;
+            RelativeSizeAxes = Axes.Both;
+            Scale = new Vector2(0.5f);
+
+            InternalChildren = new[]
+            {
+                explosion1 = new Sprite
+                {
+                    Anchor = Anchor.BottomCentre,
+                    Origin = Anchor.CentreLeft,
+                    Alpha = 0,
+                    Blending = BlendingParameters.Additive,
+                    Rotation = -90
+                },
+                explosion2 = new Sprite
+                {
+                    Anchor = Anchor.BottomCentre,
+                    Origin = Anchor.CentreLeft,
+                    Alpha = 0,
+                    Blending = BlendingParameters.Additive,
+                    Rotation = -90
+                }
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(SkinManager skins)
+        {
+            var defaultLegacySkin = skins.DefaultLegacySkin;
+
+            // sprite names intentionally swapped to match stable member naming / ease of cross-referencing
+            explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
+            explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1");
+        }
+
+        public void Animate(HitExplosionEntry entry)
+        {
+            Colour = entry.ObjectColour;
+
+            using (BeginAbsoluteSequence(entry.LifetimeStart))
+            {
+                float halfCatchWidth = catcher.CatchWidth / 2;
+                float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3);
+
+                if (!(entry.HitObject is Droplet))
+                {
+                    float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
+
+                    explosion1.Scale = new Vector2(1, 0.9f);
+                    explosion1.Position = new Vector2(explosionOffset, 0);
+
+                    explosion1.FadeOutFromOne(300);
+                    explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out);
+                }
+
+                explosion2.Scale = new Vector2(0.9f, 1);
+                explosion2.Position = new Vector2(explosionOffset, 0);
+
+                explosion2.FadeOutFromOne(700);
+                explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out);
+
+                this.Delay(700).FadeOutFromOne();
+            }
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9fd4610e6e..5cd85aac56 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -23,6 +23,7 @@ using osuTK.Graphics;
 
 namespace osu.Game.Rulesets.Catch.UI
 {
+    [Cached]
     public class Catcher : SkinReloadableDrawable
     {
         /// <summary>
@@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI
         /// <summary>
         /// Width of the area that can be used to attempt catches during gameplay.
         /// </summary>
-        private readonly float catchWidth;
+        public readonly float CatchWidth;
 
         private readonly SkinnableCatcher body;
 
@@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
             if (difficulty != null)
                 Scale = calculateScale(difficulty);
 
-            catchWidth = CalculateCatchWidth(Scale);
+            CatchWidth = CalculateCatchWidth(Scale);
 
             InternalChildren = new Drawable[]
             {
@@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI
             if (!(hitObject is PalpableCatchHitObject fruit))
                 return false;
 
-            float halfCatchWidth = catchWidth * 0.5f;
+            float halfCatchWidth = CatchWidth * 0.5f;
             return fruit.EffectiveX >= X - halfCatchWidth &&
                    fruit.EffectiveX <= X + halfCatchWidth;
         }
@@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
                     placeCaughtObject(palpableObject, positionInStack);
 
                 if (hitLighting.Value)
-                    addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
+                    addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
             }
 
             // droplet doesn't affect the catcher state
@@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
             return position;
         }
 
-        private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
-            hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
+        private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
+            hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
 
         private CaughtObject getCaughtObject(PalpableCatchHitObject source)
         {
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
index d9ab428231..955b1e6edb 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs
@@ -1,129 +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.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Skinning.Default;
 using osu.Game.Rulesets.Objects.Pooling;
-using osu.Game.Utils;
-using osuTK;
-using osuTK.Graphics;
+using osu.Game.Skinning;
+
+#nullable enable
 
 namespace osu.Game.Rulesets.Catch.UI
 {
     public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
     {
-        private readonly CircularContainer largeFaint;
-        private readonly CircularContainer smallFaint;
-        private readonly CircularContainer directionalGlow1;
-        private readonly CircularContainer directionalGlow2;
+        private readonly SkinnableDrawable skinnableExplosion;
 
         public HitExplosion()
         {
-            Size = new Vector2(20);
-            Anchor = Anchor.TopCentre;
+            RelativeSizeAxes = Axes.Both;
+            Anchor = Anchor.BottomCentre;
             Origin = Anchor.BottomCentre;
 
-            // scale roughly in-line with visual appearance of notes
-            const float initial_height = 10;
-
-            InternalChildren = new Drawable[]
+            InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
             {
-                largeFaint = new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Blending = BlendingParameters.Additive,
-                },
-                smallFaint = new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Blending = BlendingParameters.Additive,
-                },
-                directionalGlow1 = new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Size = new Vector2(0.01f, initial_height),
-                    Blending = BlendingParameters.Additive,
-                },
-                directionalGlow2 = new CircularContainer
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    RelativeSizeAxes = Axes.Both,
-                    Masking = true,
-                    Size = new Vector2(0.01f, initial_height),
-                    Blending = BlendingParameters.Additive,
-                }
+                CentreComponent = false,
+                Anchor = Anchor.BottomCentre,
+                Origin = Anchor.BottomCentre
             };
         }
 
         protected override void OnApply(HitExplosionEntry entry)
         {
-            X = entry.Position;
-            Scale = new Vector2(entry.Scale);
-            setColour(entry.ObjectColour);
-
-            using (BeginAbsoluteSequence(entry.LifetimeStart))
-                applyTransforms(entry.RNGSeed);
+            base.OnApply(entry);
+            if (IsLoaded)
+                apply(entry);
         }
 
-        private void applyTransforms(int randomSeed)
+        protected override void LoadComplete()
         {
+            base.LoadComplete();
+            apply(Entry);
+        }
+
+        private void apply(HitExplosionEntry? entry)
+        {
+            if (entry == null)
+                return;
+
+            ApplyTransformsAt(double.MinValue, true);
             ClearTransforms(true);
 
-            const double duration = 400;
-
-            // we want our size to be very small so the glow dominates it.
-            largeFaint.Size = new Vector2(0.8f);
-            largeFaint
-                .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
-                .FadeOut(duration * 2);
-
-            const float angle_variangle = 15; // should be less than 45
-            directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
-            directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
-
-            this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
-        }
-
-        private void setColour(Color4 objectColour)
-        {
-            const float roundness = 100;
-
-            largeFaint.EdgeEffect = new EdgeEffectParameters
-            {
-                Type = EdgeEffectType.Glow,
-                Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
-                Roundness = 160,
-                Radius = 200,
-            };
-
-            smallFaint.EdgeEffect = new EdgeEffectParameters
-            {
-                Type = EdgeEffectType.Glow,
-                Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
-                Roundness = 20,
-                Radius = 50,
-            };
-
-            directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
-            {
-                Type = EdgeEffectType.Glow,
-                Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
-                Roundness = roundness,
-                Radius = 40,
-            };
+            (skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
+            LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
         }
     }
 }
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
index 094d88243a..6df13e52ef 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionContainer.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.Graphics;
 using osu.Framework.Graphics.Pooling;
 using osu.Game.Rulesets.Objects.Pooling;
 
@@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
 
         public HitExplosionContainer()
         {
+            RelativeSizeAxes = Axes.Both;
+
             AddInternal(pool = new DrawablePool<HitExplosion>(10));
         }
 
diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
index b142962a8a..88871c77f6 100644
--- a/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
+++ b/osu.Game.Rulesets.Catch/UI/HitExplosionEntry.cs
@@ -2,24 +2,42 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Graphics.Performance;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Judgements;
 using osuTK.Graphics;
 
+#nullable enable
+
 namespace osu.Game.Rulesets.Catch.UI
 {
     public class HitExplosionEntry : LifetimeEntry
     {
-        public readonly float Position;
-        public readonly float Scale;
-        public readonly Color4 ObjectColour;
-        public readonly int RNGSeed;
+        /// <summary>
+        /// The judgement result that triggered this explosion.
+        /// </summary>
+        public JudgementResult JudgementResult { get; }
 
-        public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed)
+        /// <summary>
+        /// The hitobject which triggered this explosion.
+        /// </summary>
+        public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject;
+
+        /// <summary>
+        /// The accent colour of the object caught.
+        /// </summary>
+        public Color4 ObjectColour { get; }
+
+        /// <summary>
+        /// The position at which the object was caught.
+        /// </summary>
+        public float Position { get; }
+
+        public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position)
         {
             LifetimeStart = startTime;
             Position = position;
-            Scale = scale;
+            JudgementResult = judgementResult;
             ObjectColour = objectColour;
-            RNGSeed = rngSeed;
         }
     }
 }
diff --git a/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..c744c00d9a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/IHitExplosion.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+    /// <summary>
+    /// Common interface for all hit explosion skinnables.
+    /// </summary>
+    public interface IHitExplosion
+    {
+        /// <summary>
+        /// Begins animating this <see cref="IHitExplosion"/>.
+        /// </summary>
+        void Animate(HitExplosionEntry entry);
+    }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 636cd63c69..3102db270e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Sprites;
 using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu.Objects;
@@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
 
         public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
         {
-            drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap));
+            drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield, drawableRuleset.Beatmap));
         }
 
         public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
 
             protected override void Update()
             {
-                float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
-                float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
+                float start, end;
+
+                if (Precision.AlmostEquals(restrictTo.Rotation, 0))
+                {
+                    start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
+                    end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
+                }
+                else
+                {
+                    float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
+                    float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
+
+                    start = center - halfDiagonal;
+                    end = center + halfDiagonal;
+                }
 
                 float rawWidth = end - start;
 
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
index f6fd3e36ab..587ff4b573 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs
@@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
 using osu.Game.Configuration;
 using osu.Game.Rulesets.Osu.UI.Cursor;
 using osu.Game.Skinning;
+using osuTK;
 
 namespace osu.Game.Rulesets.Osu.Skinning.Legacy
 {
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         private double lastTrailTime;
         private IBindable<float> cursorSize;
 
+        private Vector2? currentPosition;
+
         public LegacyCursorTrail(ISkin skin)
         {
             this.skin = skin;
@@ -54,22 +57,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
         }
 
         protected override double FadeDuration => disjointTrail ? 150 : 500;
+        protected override float FadeExponent => 1;
 
         protected override bool InterpolateMovements => !disjointTrail;
 
         protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
 
+        protected override void Update()
+        {
+            base.Update();
+
+            if (!disjointTrail || !currentPosition.HasValue)
+                return;
+
+            if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
+            {
+                lastTrailTime = Time.Current;
+                AddTrail(currentPosition.Value);
+            }
+        }
+
         protected override bool OnMouseMove(MouseMoveEvent e)
         {
             if (!disjointTrail)
                 return base.OnMouseMove(e);
 
-            if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
-            {
-                lastTrailTime = Time.Current;
-                return base.OnMouseMove(e);
-            }
+            currentPosition = e.ScreenSpaceMousePosition;
 
+            // Intentionally block the base call as we're adding the trails ourselves.
             return false;
         }
     }
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 7f86e9daf7..7a95111c91 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
     {
         private const int max_sprites = 2048;
 
+        /// <summary>
+        /// An exponentiating factor to ease the trail fade.
+        /// </summary>
+        protected virtual float FadeExponent => 1.7f;
+
         private readonly TrailPart[] parts = new TrailPart[max_sprites];
         private int currentIndex;
         private IShader shader;
@@ -141,22 +146,25 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
 
         protected override bool OnMouseMove(MouseMoveEvent e)
         {
-            Vector2 pos = e.ScreenSpaceMousePosition;
+            AddTrail(e.ScreenSpaceMousePosition);
+            return base.OnMouseMove(e);
+        }
 
-            if (lastPosition == null)
+        protected void AddTrail(Vector2 position)
+        {
+            if (InterpolateMovements)
             {
-                lastPosition = pos;
-                resampler.AddPosition(lastPosition.Value);
-                return base.OnMouseMove(e);
-            }
-
-            foreach (Vector2 pos2 in resampler.AddPosition(pos))
-            {
-                Trace.Assert(lastPosition.HasValue);
-
-                if (InterpolateMovements)
+                if (!lastPosition.HasValue)
                 {
-                    // ReSharper disable once PossibleInvalidOperationException
+                    lastPosition = position;
+                    resampler.AddPosition(lastPosition.Value);
+                    return;
+                }
+
+                foreach (Vector2 pos2 in resampler.AddPosition(position))
+                {
+                    Trace.Assert(lastPosition.HasValue);
+
                     Vector2 pos1 = lastPosition.Value;
                     Vector2 diff = pos2 - pos1;
                     float distance = diff.Length;
@@ -170,14 +178,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
                         addPart(lastPosition.Value);
                     }
                 }
-                else
-                {
-                    lastPosition = pos2;
-                    addPart(lastPosition.Value);
-                }
             }
-
-            return base.OnMouseMove(e);
+            else
+            {
+                lastPosition = position;
+                addPart(lastPosition.Value);
+            }
         }
 
         private void addPart(Vector2 screenSpacePosition)
@@ -206,10 +212,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
             private Texture texture;
 
             private float time;
+            private float fadeExponent;
 
             private readonly TrailPart[] parts = new TrailPart[max_sprites];
             private Vector2 size;
-
             private Vector2 originPosition;
 
             private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
@@ -227,6 +233,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
                 texture = Source.texture;
                 size = Source.partSize;
                 time = Source.time;
+                fadeExponent = Source.FadeExponent;
 
                 originPosition = Vector2.Zero;
 
@@ -249,6 +256,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
 
                 shader.Bind();
                 shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
+                shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
 
                 texture.TextureGL.Bind();
 
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
index 0ec21a4c7b..5e22101e5c 100644
--- a/osu.Game.Tests/Chat/TestSceneChannelManager.cs
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.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.Collections.Generic;
 using System.Linq;
 using NUnit.Framework;
 using osu.Framework.Allocation;
@@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
     {
         private ChannelManager channelManager;
         private int currentMessageId;
+        private List<Message> sentMessages;
 
         [SetUp]
         public void Setup() => Schedule(() =>
@@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
             AddStep("register request handling", () =>
             {
                 currentMessageId = 0;
+                sentMessages = new List<Message>();
 
                 ((DummyAPIAccess)API).HandleRequest = req =>
                 {
@@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
                             return true;
 
                         case PostMessageRequest postMessage:
-                            postMessage.TriggerSuccess(new Message(++currentMessageId)
-                            {
-                                IsAction = postMessage.Message.IsAction,
-                                ChannelId = postMessage.Message.ChannelId,
-                                Content = postMessage.Message.Content,
-                                Links = postMessage.Message.Links,
-                                Timestamp = postMessage.Message.Timestamp,
-                                Sender = postMessage.Message.Sender
-                            });
+                            handlePostMessageRequest(postMessage);
+                            return true;
 
+                        case MarkChannelAsReadRequest markRead:
+                            handleMarkChannelAsReadRequest(markRead);
                             return true;
                     }
 
@@ -83,12 +81,65 @@ namespace osu.Game.Tests.Chat
             AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
         }
 
+        [Test]
+        public void TestMarkAsReadIgnoringLocalMessages()
+        {
+            Channel channel = null;
+
+            AddStep("join channel and select it", () =>
+            {
+                channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
+                channelManager.CurrentChannel.Value = channel;
+            });
+
+            AddStep("post message", () => channelManager.PostMessage("Something interesting"));
+
+            AddStep("post /help command", () => channelManager.PostCommand("help", channel));
+            AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
+            AddStep("post /join command with no channel", () => channelManager.PostCommand("join", channel));
+            AddStep("post /join command with non-existent channel", () => channelManager.PostCommand("join i-dont-exist", channel));
+            AddStep("post non-existent command", () => channelManager.PostCommand("non-existent-cmd arg", channel));
+
+            AddStep("mark channel as read", () => channelManager.MarkChannelAsRead(channel));
+            AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
+        }
+
+        private void handlePostMessageRequest(PostMessageRequest request)
+        {
+            var message = new Message(++currentMessageId)
+            {
+                IsAction = request.Message.IsAction,
+                ChannelId = request.Message.ChannelId,
+                Content = request.Message.Content,
+                Links = request.Message.Links,
+                Timestamp = request.Message.Timestamp,
+                Sender = request.Message.Sender
+            };
+
+            sentMessages.Add(message);
+            request.TriggerSuccess(message);
+        }
+
+        private void handleMarkChannelAsReadRequest(MarkChannelAsReadRequest request)
+        {
+            // only accept messages that were sent through the API
+            if (sentMessages.Contains(request.Message))
+            {
+                request.TriggerSuccess();
+            }
+            else
+            {
+                request.TriggerFailure(new APIException("unknown message!", null));
+            }
+        }
+
         private Channel createChannel(int id, ChannelType type) => new Channel(new User())
         {
             Id = id,
             Name = $"Channel {id}",
             Topic = $"Topic of channel {id} with type {type}",
             Type = type,
+            LastMessageId = 0,
         };
 
         private class ChannelManagerContainer : CompositeDrawable
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index 17fe09f2c6..0441c5641e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
@@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay
             });
 
             AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true));
+            AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
             AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
         }
 
@@ -83,19 +84,38 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 }));
         }
 
+        [Test]
+        public void TestMaxHeight()
+        {
+            int playerNumber = 1;
+            AddRepeatStep("add 3 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 3);
+            checkHeight(4);
+
+            AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
+            checkHeight(8);
+
+            AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
+            checkHeight(8);
+
+            void checkHeight(int panelCount)
+                => AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
+        }
+
         private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
 
         private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false)
         {
-            var leaderboardScore = leaderboard.AddPlayer(user, isTracked);
+            var leaderboardScore = leaderboard.Add(user, isTracked);
             leaderboardScore.TotalScore.BindTo(score);
         }
 
         private class TestGameplayLeaderboard : GameplayLeaderboard
         {
+            public float Spacing => Flow.Spacing.Y;
+
             public bool CheckPositionByUsername(string username, int? expectedPosition)
             {
-                var scoreItem = this.FirstOrDefault(i => i.User?.Username == username);
+                var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
 
                 return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
             }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index b7e92a79a0..3017428039 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -12,6 +12,7 @@ using osu.Game.Configuration;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Screens.Play;
+using osu.Game.Skinning;
 using osuTK.Input;
 
 namespace osu.Game.Tests.Visual.Gameplay
@@ -142,6 +143,22 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue));
         }
 
+        [Test]
+        public void TestHiddenHUDDoesntBlockSkinnableComponentsLoad()
+        {
+            HUDVisibilityMode originalConfigValue = default;
+
+            AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
+
+            AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+
+            createNew();
+            AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
+            AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
+
+            AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
+        }
+
         private void createNew(Action<HUDOverlay> action = null)
         {
             AddStep("create overlay", () =>
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs
new file mode 100644
index 0000000000..e58f85b0b3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs
@@ -0,0 +1,42 @@
+// 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.Overlays;
+using osu.Game.Tests.Visual.Navigation;
+
+namespace osu.Game.Tests.Visual.Menus
+{
+    public class TestSceneSideOverlays : OsuGameTestScene
+    {
+        [SetUpSteps]
+        public override void SetUpSteps()
+        {
+            base.SetUpSteps();
+
+            AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f);
+            AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded);
+        }
+
+        [Test]
+        public void TestScreenOffsettingOnSettingsOverlay()
+        {
+            AddStep("open settings", () => Game.Settings.Show());
+            AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
+
+            AddStep("hide settings", () => Game.Settings.Hide());
+            AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
+        }
+
+        [Test]
+        public void TestScreenOffsettingOnNotificationOverlay()
+        {
+            AddStep("open notifications", () => Game.Notifications.Show());
+            AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
+
+            AddStep("hide notifications", () => Game.Notifications.Hide());
+            AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs
new file mode 100644
index 0000000000..af874cec91
--- /dev/null
+++ b/osu.Game.Tests/Visual/Mods/TestSceneModFailCondition.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual.Mods
+{
+    public class TestSceneModFailCondition : ModTestScene
+    {
+        private bool restartRequested;
+
+        protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
+        protected override TestPlayer CreateModPlayer(Ruleset ruleset)
+        {
+            var player = base.CreateModPlayer(ruleset);
+            player.RestartRequested = () => restartRequested = true;
+            return player;
+        }
+
+        protected override bool AllowFail => true;
+
+        [SetUpSteps]
+        public void SetUp()
+        {
+            AddStep("reset flag", () => restartRequested = false);
+        }
+
+        [Test]
+        public void TestRestartOnFailDisabled() => CreateModTest(new ModTestData
+        {
+            Autoplay = false,
+            Mod = new OsuModSuddenDeath(),
+            PassCondition = () => !restartRequested && Player.ChildrenOfType<FailOverlay>().Single().State.Value == Visibility.Visible
+        });
+
+        [Test]
+        public void TestRestartOnFailEnabled() => CreateModTest(new ModTestData
+        {
+            Autoplay = false,
+            Mod = new OsuModSuddenDeath
+            {
+                Restart = { Value = true }
+            },
+            PassCondition = () => restartRequested && Player.ChildrenOfType<FailOverlay>().Single().State.Value == Visibility.Hidden
+        });
+    }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 8f20429bf0..08b3fb98a8 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         public void TestEmpty()
         {
             // used to test the flow of multiplayer from visual tests.
+            AddStep("empty step", () => { });
         }
 
         [Test]
@@ -408,8 +409,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
 
             AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
 
-            testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().TriggerClick());
-
             testLeave("back button", () => multiplayerScreen.OnBackButton());
 
             // mimics home button and OS window close
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs
new file mode 100644
index 0000000000..ff06d4d9c7
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.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 System;
+using NUnit.Framework;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+    public class TestSceneMultiplayerResults : ScreenTestScene
+    {
+        [Test]
+        public void TestDisplayResults()
+        {
+            MultiplayerResultsScreen screen = null;
+
+            AddStep("show results screen", () =>
+            {
+                var rulesetInfo = new OsuRuleset().RulesetInfo;
+                var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
+
+                var score = new ScoreInfo
+                {
+                    Rank = ScoreRank.B,
+                    TotalScore = 987654,
+                    Accuracy = 0.8,
+                    MaxCombo = 500,
+                    Combo = 250,
+                    Beatmap = beatmapInfo,
+                    User = new User { Username = "Test user" },
+                    Date = DateTimeOffset.Now,
+                    OnlineScoreID = 12345,
+                    Ruleset = rulesetInfo,
+                };
+
+                PlaylistItem playlistItem = new PlaylistItem
+                {
+                    BeatmapID = beatmapInfo.ID,
+                };
+
+                Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem));
+            });
+
+            AddUntilStep("wait for loaded", () => screen.IsLoaded);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
new file mode 100644
index 0000000000..0a8bda7ec0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs
@@ -0,0 +1,61 @@
+// 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 NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Game.Online.Rooms;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+    public class TestSceneMultiplayerTeamResults : ScreenTestScene
+    {
+        [TestCase(7483253, 1048576)]
+        [TestCase(1048576, 7483253)]
+        [TestCase(1048576, 1048576)]
+        public void TestDisplayTeamResults(int team1Score, int team2Score)
+        {
+            MultiplayerResultsScreen screen = null;
+
+            AddStep("show results screen", () =>
+            {
+                var rulesetInfo = new OsuRuleset().RulesetInfo;
+                var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
+
+                var score = new ScoreInfo
+                {
+                    Rank = ScoreRank.B,
+                    TotalScore = 987654,
+                    Accuracy = 0.8,
+                    MaxCombo = 500,
+                    Combo = 250,
+                    Beatmap = beatmapInfo,
+                    User = new User { Username = "Test user" },
+                    Date = DateTimeOffset.Now,
+                    OnlineScoreID = 12345,
+                    Ruleset = rulesetInfo,
+                };
+
+                PlaylistItem playlistItem = new PlaylistItem
+                {
+                    BeatmapID = beatmapInfo.ID,
+                };
+
+                SortedDictionary<int, BindableInt> teamScores = new SortedDictionary<int, BindableInt>
+                {
+                    { 0, new BindableInt(team1Score) },
+                    { 1, new BindableInt(team2Score) }
+                };
+
+                Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores));
+            });
+
+            AddUntilStep("wait for loaded", () => screen.IsLoaded);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
index f9a991f756..c9a1471e41 100644
--- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
+++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
 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.Platform;
 using osu.Framework.Screens;
@@ -95,6 +96,8 @@ namespace osu.Game.Tests.Visual.Navigation
 
         public class TestOsuGame : OsuGame
         {
+            public new const float SIDE_OVERLAY_OFFSET_RATIO = OsuGame.SIDE_OVERLAY_OFFSET_RATIO;
+
             public new ScreenStack ScreenStack => base.ScreenStack;
 
             public new BackButton BackButton => base.BackButton;
@@ -103,7 +106,11 @@ namespace osu.Game.Tests.Visual.Navigation
 
             public new ScoreManager ScoreManager => base.ScoreManager;
 
-            public new SettingsPanel Settings => base.Settings;
+            public new Container ScreenOffsetContainer => base.ScreenOffsetContainer;
+
+            public new SettingsOverlay Settings => base.Settings;
+
+            public new NotificationOverlay Notifications => base.Notifications;
 
             public new MusicController MusicController => base.MusicController;
 
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs
deleted file mode 100644
index 40e191dd7e..0000000000
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs
+++ /dev/null
@@ -1,23 +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.Game.Screens.OnlinePlay.Lounge.Components;
-
-namespace osu.Game.Tests.Visual.Playlists
-{
-    public class TestScenePlaylistsFilterControl : OsuTestScene
-    {
-        public TestScenePlaylistsFilterControl()
-        {
-            Child = new PlaylistsFilterControl
-            {
-                Anchor = Anchor.Centre,
-                Origin = Anchor.Centre,
-                RelativeSizeAxes = Axes.X,
-                Width = 0.7f,
-                Height = 80,
-            };
-        }
-    }
-}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index ecdb046203..aff0e7ba4b 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -62,6 +62,24 @@ namespace osu.Game.Tests.Visual.Playlists
             AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1]));
         }
 
+        [Test]
+        public void TestEnteringRoomTakesLeaseOnSelection()
+        {
+            AddStep("add rooms", () => RoomManager.AddRooms(1));
+
+            AddAssert("selected room is not disabled", () => !OnlinePlayDependencies.SelectedRoom.Disabled);
+
+            AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick());
+            AddAssert("selected room is non-null", () => OnlinePlayDependencies.SelectedRoom.Value != null);
+
+            AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick());
+
+            AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen);
+
+            AddAssert("selected room is non-null", () => OnlinePlayDependencies.SelectedRoom.Value != null);
+            AddAssert("selected room is disabled", () => OnlinePlayDependencies.SelectedRoom.Disabled);
+        }
+
         private bool checkRoomVisible(DrawableRoom room) =>
             loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
                         .Contains(room.ScreenSpaceDrawQuad.Centre);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index a62980addf..da474a64ba 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Settings
                 new TabletSettings(tabletHandler)
                 {
                     RelativeSizeAxes = Axes.None,
-                    Width = SettingsPanel.WIDTH,
+                    Width = SettingsPanel.PANEL_WIDTH,
                     Anchor = Anchor.TopCentre,
                     Origin = Anchor.TopCentre,
                 }
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 60a0d5a0ac..6c7adcc806 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -101,7 +101,7 @@ namespace osu.Game.Configuration
             SetDefault(OsuSetting.HitLighting, true);
 
             SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
-            SetDefault(OsuSetting.ShowProgressGraph, true);
+            SetDefault(OsuSetting.ShowDifficultyGraph, true);
             SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
             SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
             SetDefault(OsuSetting.KeyOverlay, false);
@@ -217,7 +217,7 @@ namespace osu.Game.Configuration
         AlwaysPlayFirstComboBreak,
         FloatingComments,
         HUDVisibilityMode,
-        ShowProgressGraph,
+        ShowDifficultyGraph,
         ShowHealthDisplayWhenCantFail,
         FadePlayfieldWhenHealthLow,
         MouseDisableButtons,
diff --git a/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.cs
new file mode 100644
index 0000000000..111c068bbd
--- /dev/null
+++ b/osu.Game/Localisation/MultiplayerTeamResultsScreenStrings.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.Localisation;
+
+namespace osu.Game.Localisation
+{
+    public static class MultiplayerTeamResultsScreenStrings
+    {
+        private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerTeamResultsScreen";
+
+        /// <summary>
+        /// "Team {0} wins!"
+        /// </summary>
+        public static LocalisableString TeamWins(string winner) => new TranslatableString(getKey(@"team_wins"), @"Team {0} wins!", winner);
+
+        /// <summary>
+        /// "The teams are tied!"
+        /// </summary>
+        public static LocalisableString TheTeamsAreTied => new TranslatableString(getKey(@"the_teams_are_tied"), @"The teams are tied!");
+
+        private static string getKey(string key) => $@"{prefix}:{key}";
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs
index 95a5d0acbd..b24669e6d5 100644
--- a/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs
+++ b/osu.Game/Online/API/Requests/MarkChannelAsReadRequest.cs
@@ -9,16 +9,16 @@ namespace osu.Game.Online.API.Requests
 {
     public class MarkChannelAsReadRequest : APIRequest
     {
-        private readonly Channel channel;
-        private readonly Message message;
+        public readonly Channel Channel;
+        public readonly Message Message;
 
         public MarkChannelAsReadRequest(Channel channel, Message message)
         {
-            this.channel = channel;
-            this.message = message;
+            Channel = channel;
+            Message = message;
         }
 
-        protected override string Target => $"chat/channels/{channel.Id}/mark-as-read/{message.Id}";
+        protected override string Target => $"chat/channels/{Channel.Id}/mark-as-read/{Message.Id}";
 
         protected override WebRequest CreateWebRequest()
         {
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 3136a3960d..1937019ef6 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -553,7 +553,7 @@ namespace osu.Game.Online.Chat
             if (channel.LastMessageId == channel.LastReadId)
                 return;
 
-            var message = channel.Messages.LastOrDefault();
+            var message = channel.Messages.FindLast(msg => !(msg is LocalMessage));
 
             if (message == null)
                 return;
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
index 064065ab00..8f16d22c4c 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs
@@ -31,6 +31,15 @@ namespace osu.Game.Online.Multiplayer
         /// <param name="user">The user.</param>
         Task UserLeft(MultiplayerRoomUser user);
 
+        /// <summary>
+        /// Signals that a user has been kicked from the room.
+        /// </summary>
+        /// <remarks>
+        /// This will also be sent to the user that was kicked.
+        /// </remarks>
+        /// <param name="user">The user.</param>
+        Task UserKicked(MultiplayerRoomUser user);
+
         /// <summary>
         /// Signal that the host of the room has changed.
         /// </summary>
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 4607211cdf..2a0635c98c 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -389,6 +389,18 @@ namespace osu.Game.Online.Multiplayer
             return Task.CompletedTask;
         }
 
+        Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user)
+        {
+            if (LocalUser == null)
+                return Task.CompletedTask;
+
+            if (user.Equals(LocalUser))
+                LeaveRoom();
+
+            // TODO: also inform users of the kick operation.
+            return ((IMultiplayerClient)this).UserLeft(user);
+        }
+
         Task IMultiplayerClient.HostChanged(int userId)
         {
             if (Room == null)
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index 55477a9fc7..c38a648a6a 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer
                     connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
                     connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
                     connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
+                    connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
                     connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
                     connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
                     connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 3cfa2cc755..fb682e0909 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -64,6 +64,11 @@ namespace osu.Game
     /// </summary>
     public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>
     {
+        /// <summary>
+        /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications).
+        /// </summary>
+        protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f;
+
         public Toolbar Toolbar;
 
         private ChatOverlay chatOverlay;
@@ -71,7 +76,7 @@ namespace osu.Game
         private ChannelManager channelManager;
 
         [NotNull]
-        private readonly NotificationOverlay notifications = new NotificationOverlay();
+        protected readonly NotificationOverlay Notifications = new NotificationOverlay();
 
         private BeatmapListingOverlay beatmapListing;
 
@@ -97,7 +102,7 @@ namespace osu.Game
 
         private ScalingContainer screenContainer;
 
-        private Container screenOffsetContainer;
+        protected Container ScreenOffsetContainer { get; private set; }
 
         [Resolved]
         private FrameworkConfigManager frameworkConfig { get; set; }
@@ -312,7 +317,7 @@ namespace osu.Game
                 case LinkAction.OpenEditorTimestamp:
                 case LinkAction.JoinMultiplayerMatch:
                 case LinkAction.Spectate:
-                    waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification
+                    waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
                     {
                         Text = @"This link type is not yet supported!",
                         Icon = FontAwesome.Solid.LifeRing,
@@ -611,12 +616,12 @@ namespace osu.Game
             MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
 
             // todo: all archive managers should be able to be looped here.
-            SkinManager.PostNotification = n => notifications.Post(n);
+            SkinManager.PostNotification = n => Notifications.Post(n);
 
-            BeatmapManager.PostNotification = n => notifications.Post(n);
+            BeatmapManager.PostNotification = n => Notifications.Post(n);
             BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
 
-            ScoreManager.PostNotification = n => notifications.Post(n);
+            ScoreManager.PostNotification = n => Notifications.Post(n);
             ScoreManager.PresentImport = items => PresentScore(items.First());
 
             // make config aware of how to lookup skins for on-screen display purposes.
@@ -655,7 +660,7 @@ namespace osu.Game
                     ActionRequested = action => volume.Adjust(action),
                     ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
                 },
-                screenOffsetContainer = new Container
+                ScreenOffsetContainer = new Container
                 {
                     RelativeSizeAxes = Axes.Both,
                     Children = new Drawable[]
@@ -724,7 +729,7 @@ namespace osu.Game
 
             loadComponentSingleFile(onScreenDisplay, Add, true);
 
-            loadComponentSingleFile(notifications.With(d =>
+            loadComponentSingleFile(Notifications.With(d =>
             {
                 d.GetToolbarHeight = () => ToolbarOffset;
                 d.Anchor = Anchor.TopRight;
@@ -733,7 +738,7 @@ namespace osu.Game
 
             loadComponentSingleFile(new CollectionManager(Storage)
             {
-                PostNotification = n => notifications.Post(n),
+                PostNotification = n => Notifications.Post(n),
             }, Add, true);
 
             loadComponentSingleFile(stableImportManager, Add);
@@ -785,7 +790,7 @@ namespace osu.Game
             Add(new MusicKeyBindingHandler());
 
             // side overlays which cancel each other.
-            var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
+            var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications };
 
             foreach (var overlay in singleDisplaySideOverlays)
             {
@@ -828,21 +833,6 @@ namespace osu.Game
             {
                 if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
             };
-
-            void updateScreenOffset()
-            {
-                float offset = 0;
-
-                if (Settings.State.Value == Visibility.Visible)
-                    offset += Toolbar.HEIGHT / 2;
-                if (notifications.State.Value == Visibility.Visible)
-                    offset -= Toolbar.HEIGHT / 2;
-
-                screenOffsetContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
-            }
-
-            Settings.State.ValueChanged += _ => updateScreenOffset();
-            notifications.State.ValueChanged += _ => updateScreenOffset();
         }
 
         private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
@@ -874,7 +864,7 @@ namespace osu.Game
 
                 if (recentLogCount < short_term_display_limit)
                 {
-                    Schedule(() => notifications.Post(new SimpleErrorNotification
+                    Schedule(() => Notifications.Post(new SimpleErrorNotification
                     {
                         Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
                         Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
@@ -882,7 +872,7 @@ namespace osu.Game
                 }
                 else if (recentLogCount == short_term_display_limit)
                 {
-                    Schedule(() => notifications.Post(new SimpleNotification
+                    Schedule(() => Notifications.Post(new SimpleNotification
                     {
                         Icon = FontAwesome.Solid.EllipsisH,
                         Text = "Subsequent messages have been logged. Click to view log files.",
@@ -1023,9 +1013,18 @@ namespace osu.Game
         {
             base.UpdateAfterChildren();
 
-            screenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
+            ScreenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
             overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
 
+            var horizontalOffset = 0f;
+
+            if (Settings.IsLoaded && Settings.IsPresent)
+                horizontalOffset += ToLocalSpace(Settings.ScreenSpaceDrawQuad.TopRight).X * SIDE_OVERLAY_OFFSET_RATIO;
+            if (Notifications.IsLoaded && Notifications.IsPresent)
+                horizontalOffset += (ToLocalSpace(Notifications.ScreenSpaceDrawQuad.TopLeft).X - DrawWidth) * SIDE_OVERLAY_OFFSET_RATIO;
+
+            ScreenOffsetContainer.X = horizontalOffset;
+
             MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
         }
 
diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
index 3f1034759e..757698e1aa 100644
--- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs
+++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
@@ -78,10 +78,10 @@ namespace osu.Game.Overlays.BeatmapSet
                 Direction = FillDirection.Horizontal,
                 Children = new[]
                 {
-                    length = new Statistic(FontAwesome.Regular.Clock, "Length") { Width = 0.25f },
-                    bpm = new Statistic(FontAwesome.Regular.Circle, "BPM") { Width = 0.25f },
-                    circleCount = new Statistic(FontAwesome.Regular.Circle, "Circle Count") { Width = 0.25f },
-                    sliderCount = new Statistic(FontAwesome.Regular.Circle, "Slider Count") { Width = 0.25f },
+                    length = new Statistic(BeatmapStatisticsIconType.Length, "Length") { Width = 0.25f },
+                    bpm = new Statistic(BeatmapStatisticsIconType.Bpm, "BPM") { Width = 0.25f },
+                    circleCount = new Statistic(BeatmapStatisticsIconType.Circles, "Circle Count") { Width = 0.25f },
+                    sliderCount = new Statistic(BeatmapStatisticsIconType.Sliders, "Slider Count") { Width = 0.25f },
                 },
             };
         }
@@ -104,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapSet
                 set => this.value.Text = value;
             }
 
-            public Statistic(IconUsage icon, string name)
+            public Statistic(BeatmapStatisticsIconType icon, string name)
             {
                 TooltipText = name;
                 RelativeSizeAxes = Axes.X;
@@ -133,8 +133,16 @@ namespace osu.Game.Overlays.BeatmapSet
                             {
                                 Anchor = Anchor.CentreLeft,
                                 Origin = Anchor.Centre,
-                                Icon = icon,
-                                Size = new Vector2(12),
+                                Icon = FontAwesome.Regular.Circle,
+                                Size = new Vector2(10),
+                                Rotation = 0,
+                                Colour = Color4Extensions.FromHex(@"f7dd55"),
+                            },
+                            new BeatmapStatisticIcon(icon)
+                            {
+                                Anchor = Anchor.CentreLeft,
+                                Origin = Anchor.Centre,
+                                Size = new Vector2(10),
                                 Colour = Color4Extensions.FromHex(@"f7dd55"),
                                 Scale = new Vector2(0.8f),
                             },
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index b26e17b34c..e3956089c2 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Overlays
         public LocalisableString Title => NotificationsStrings.HeaderTitle;
         public LocalisableString Description => NotificationsStrings.HeaderDescription;
 
-        private const float width = 320;
+        public const float WIDTH = 320;
 
         public const float TRANSITION_LENGTH = 600;
 
@@ -38,7 +38,8 @@ namespace osu.Game.Overlays
         [BackgroundDependencyLoader]
         private void load()
         {
-            Width = width;
+            X = WIDTH;
+            Width = WIDTH;
             RelativeSizeAxes = Axes.Y;
 
             Children = new Drawable[]
@@ -152,7 +153,7 @@ namespace osu.Game.Overlays
 
             markAllRead();
 
-            this.MoveToX(width, TRANSITION_LENGTH, Easing.OutQuint);
+            this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint);
             this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
         }
 
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 353292606f..69aa57082a 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
                 new SettingsCheckbox
                 {
                     LabelText = "Show difficulty graph on progress bar",
-                    Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph)
+                    Current = config.GetBindable<bool>(OsuSetting.ShowDifficultyGraph)
                 },
                 new SettingsCheckbox
                 {
diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs
index 54b780615d..55e8aee266 100644
--- a/osu.Game/Overlays/SettingsOverlay.cs
+++ b/osu.Game/Overlays/SettingsOverlay.cs
@@ -9,7 +9,6 @@ using osu.Game.Overlays.Settings.Sections;
 using osu.Game.Overlays.Settings.Sections.Input;
 using osuTK.Graphics;
 using System.Collections.Generic;
-using System.Linq;
 using osu.Framework.Bindables;
 using osu.Framework.Localisation;
 using osu.Game.Localisation;
@@ -38,6 +37,8 @@ namespace osu.Game.Overlays
 
         private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>();
 
+        private SettingsSubPanel lastOpenedSubPanel;
+
         protected override Drawable CreateHeader() => new SettingsHeader(Title, Description);
         protected override Drawable CreateFooter() => new SettingsFooter();
 
@@ -46,21 +47,21 @@ namespace osu.Game.Overlays
         {
         }
 
-        public override bool AcceptsFocus => subPanels.All(s => s.State.Value != Visibility.Visible);
+        public override bool AcceptsFocus => lastOpenedSubPanel == null || lastOpenedSubPanel.State.Value == Visibility.Hidden;
 
         private T createSubPanel<T>(T subPanel)
             where T : SettingsSubPanel
         {
             subPanel.Depth = 1;
             subPanel.Anchor = Anchor.TopRight;
-            subPanel.State.ValueChanged += subPanelStateChanged;
+            subPanel.State.ValueChanged += e => subPanelStateChanged(subPanel, e);
 
             subPanels.Add(subPanel);
 
             return subPanel;
         }
 
-        private void subPanelStateChanged(ValueChangedEvent<Visibility> state)
+        private void subPanelStateChanged(SettingsSubPanel panel, ValueChangedEvent<Visibility> state)
         {
             switch (state.NewValue)
             {
@@ -68,7 +69,9 @@ namespace osu.Game.Overlays
                     Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint);
 
                     SectionsContainer.FadeOut(300, Easing.OutQuint);
-                    ContentContainer.MoveToX(-WIDTH, 500, Easing.OutQuint);
+                    ContentContainer.MoveToX(-PANEL_WIDTH, 500, Easing.OutQuint);
+
+                    lastOpenedSubPanel = panel;
                     break;
 
                 case Visibility.Hidden:
@@ -80,7 +83,7 @@ namespace osu.Game.Overlays
             }
         }
 
-        protected override float ExpandedPosition => subPanels.Any(s => s.State.Value == Visibility.Visible) ? -WIDTH : base.ExpandedPosition;
+        protected override float ExpandedPosition => lastOpenedSubPanel?.State.Value == Visibility.Visible ? -PANEL_WIDTH : base.ExpandedPosition;
 
         [BackgroundDependencyLoader]
         private void load()
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index eae828c142..f1c41c4b50 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -28,7 +28,15 @@ namespace osu.Game.Overlays
 
         private const float sidebar_width = Sidebar.DEFAULT_WIDTH;
 
-        public const float WIDTH = 400;
+        /// <summary>
+        /// The width of the settings panel content, excluding the sidebar.
+        /// </summary>
+        public const float PANEL_WIDTH = 400;
+
+        /// <summary>
+        /// The full width of the settings panel, including the sidebar.
+        /// </summary>
+        public const float WIDTH = sidebar_width + PANEL_WIDTH;
 
         protected Container<Drawable> ContentContainer;
 
@@ -64,7 +72,8 @@ namespace osu.Game.Overlays
         {
             InternalChild = ContentContainer = new NonMaskedContent
             {
-                Width = WIDTH,
+                X = -WIDTH + ExpandedPosition,
+                Width = PANEL_WIDTH,
                 RelativeSizeAxes = Axes.Y,
                 Children = new Drawable[]
                 {
diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs
index c0d7bae2b2..4425ece513 100644
--- a/osu.Game/Rulesets/Mods/ModFailCondition.cs
+++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs
@@ -2,6 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
 using osu.Game.Rulesets.Judgements;
 using osu.Game.Rulesets.Scoring;
 
@@ -11,9 +13,12 @@ namespace osu.Game.Rulesets.Mods
     {
         public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
 
+        [SettingSource("Restart on fail", "Automatically restarts when failed.")]
+        public BindableBool Restart { get; } = new BindableBool();
+
         public virtual bool PerformFail() => true;
 
-        public virtual bool RestartOnFail => true;
+        public virtual bool RestartOnFail => Restart.Value;
 
         public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
         {
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 187a4d8e23..9016a24f8d 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Mods
 
         public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
 
+        protected ModPerfect()
+        {
+            Restart.Value = Restart.Default = true;
+        }
+
         protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
             => result.Type.AffectsAccuracy()
                && result.Type != result.Judgement.MaxResult;
diff --git a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
index d8dfac496d..e2ba0b03b0 100644
--- a/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs
@@ -10,12 +10,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
 {
     public class OnlinePlayBackgroundSprite : OnlinePlayComposite
     {
-        private readonly BeatmapSetCoverType beatmapSetCoverType;
+        protected readonly BeatmapSetCoverType BeatmapSetCoverType;
         private UpdateableBeatmapBackgroundSprite sprite;
 
         public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
         {
-            this.beatmapSetCoverType = beatmapSetCoverType;
+            BeatmapSetCoverType = beatmapSetCoverType;
         }
 
         [BackgroundDependencyLoader]
@@ -33,6 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
             sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value;
         }
 
-        protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
+        protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
     }
 }
diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs
index bf0a53cbb6..b0db9256f5 100644
--- a/osu.Game/Screens/OnlinePlay/Header.cs
+++ b/osu.Game/Screens/OnlinePlay/Header.cs
@@ -2,19 +2,15 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using Humanizer;
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Screens;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
 using osu.Game.Overlays;
 using osuTK;
-using osuTK.Graphics;
 
 namespace osu.Game.Screens.OnlinePlay
 {
@@ -22,52 +18,30 @@ namespace osu.Game.Screens.OnlinePlay
     {
         public const float HEIGHT = 80;
 
+        private readonly ScreenStack stack;
+        private readonly MultiHeaderTitle title;
+
         public Header(string mainTitle, ScreenStack stack)
         {
+            this.stack = stack;
+
             RelativeSizeAxes = Axes.X;
             Height = HEIGHT;
+            Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING };
 
-            HeaderBreadcrumbControl breadcrumbs;
-            MultiHeaderTitle title;
-
-            Children = new Drawable[]
+            Child = title = new MultiHeaderTitle(mainTitle)
             {
-                new Box
-                {
-                    RelativeSizeAxes = Axes.Both,
-                    Colour = Color4Extensions.FromHex(@"#1f1921"),
-                },
-                new Container
-                {
-                    Anchor = Anchor.CentreLeft,
-                    Origin = Anchor.CentreLeft,
-                    RelativeSizeAxes = Axes.Both,
-                    Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING },
-                    Children = new Drawable[]
-                    {
-                        title = new MultiHeaderTitle(mainTitle)
-                        {
-                            Anchor = Anchor.CentreLeft,
-                            Origin = Anchor.BottomLeft,
-                        },
-                        breadcrumbs = new HeaderBreadcrumbControl(stack)
-                        {
-                            Anchor = Anchor.BottomLeft,
-                            Origin = Anchor.BottomLeft
-                        }
-                    },
-                },
+                Anchor = Anchor.CentreLeft,
+                Origin = Anchor.CentreLeft,
             };
 
-            breadcrumbs.Current.ValueChanged += screen =>
-            {
-                if (screen.NewValue is IOnlinePlaySubScreen onlineSubScreen)
-                    title.Screen = onlineSubScreen;
-            };
-
-            breadcrumbs.Current.TriggerChange();
+            // unnecessary to unbind these as this header has the same lifetime as the screen stack we are attaching to.
+            stack.ScreenPushed += (_, __) => updateSubScreenTitle();
+            stack.ScreenExited += (_, __) => updateSubScreenTitle();
         }
 
+        private void updateSubScreenTitle() => title.Screen = stack.CurrentScreen as IOnlinePlaySubScreen;
+
         private class MultiHeaderTitle : CompositeDrawable
         {
             private const float spacing = 6;
@@ -75,9 +49,10 @@ namespace osu.Game.Screens.OnlinePlay
             private readonly OsuSpriteText dot;
             private readonly OsuSpriteText pageTitle;
 
+            [CanBeNull]
             public IOnlinePlaySubScreen Screen
             {
-                set => pageTitle.Text = value.ShortTitle.Titleize();
+                set => pageTitle.Text = value?.ShortTitle.Titleize() ?? string.Empty;
             }
 
             public MultiHeaderTitle(string mainTitle)
@@ -125,35 +100,5 @@ namespace osu.Game.Screens.OnlinePlay
                 pageTitle.Colour = dot.Colour = colours.Yellow;
             }
         }
-
-        private class HeaderBreadcrumbControl : ScreenBreadcrumbControl
-        {
-            public HeaderBreadcrumbControl(ScreenStack stack)
-                : base(stack)
-            {
-                RelativeSizeAxes = Axes.X;
-                StripColour = Color4.Transparent;
-            }
-
-            protected override void LoadComplete()
-            {
-                base.LoadComplete();
-                AccentColour = Color4Extensions.FromHex("#e35c99");
-            }
-
-            protected override TabItem<IScreen> CreateTabItem(IScreen value) => new HeaderBreadcrumbTabItem(value)
-            {
-                AccentColour = AccentColour
-            };
-
-            private class HeaderBreadcrumbTabItem : BreadcrumbTabItem
-            {
-                public HeaderBreadcrumbTabItem(IScreen value)
-                    : base(value)
-                {
-                    Bar.Colour = Color4.Transparent;
-                }
-            }
-        }
     }
 }
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
index 193fb0cf57..c8ecd65574 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs
@@ -158,21 +158,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
             Children = new Drawable[]
             {
                 // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
-                new BufferedContainer
+                new Box
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Children = new Drawable[]
-                    {
-                        new Box
-                        {
-                            RelativeSizeAxes = Axes.Both,
-                            Colour = colours.Background5,
-                        },
-                        new OnlinePlayBackgroundSprite
-                        {
-                            RelativeSizeAxes = Axes.Both
-                        },
-                    }
+                    Colour = colours.Background5,
+                },
+                new OnlinePlayBackgroundSprite
+                {
+                    RelativeSizeAxes = Axes.Both
                 },
                 new Container
                 {
@@ -187,37 +180,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
                         CornerRadius = corner_radius,
                         Children = new Drawable[]
                         {
-                            // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
-                            new BufferedContainer
+                            new GridContainer
                             {
                                 RelativeSizeAxes = Axes.Both,
-                                Children = new Drawable[]
+                                ColumnDimensions = new[]
                                 {
-                                    new GridContainer
-                                    {
-                                        RelativeSizeAxes = Axes.Both,
-                                        ColumnDimensions = new[]
-                                        {
-                                            new Dimension(GridSizeMode.Relative, 0.2f)
-                                        },
-                                        Content = new[]
-                                        {
-                                            new Drawable[]
-                                            {
-                                                new Box
-                                                {
-                                                    RelativeSizeAxes = Axes.Both,
-                                                    Colour = colours.Background5,
-                                                },
-                                                new Box
-                                                {
-                                                    RelativeSizeAxes = Axes.Both,
-                                                    Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
-                                                },
-                                            }
-                                        }
-                                    },
+                                    new Dimension(GridSizeMode.Relative, 0.2f)
                                 },
+                                Content = new[]
+                                {
+                                    new Drawable[]
+                                    {
+                                        new Box
+                                        {
+                                            RelativeSizeAxes = Axes.Both,
+                                            Colour = colours.Background5,
+                                        },
+                                        new Box
+                                        {
+                                            RelativeSizeAxes = Axes.Both,
+                                            Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
+                                        },
+                                    }
+                                }
                             },
                             new Container
                             {
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs
deleted file mode 100644
index e2f02fca68..0000000000
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs
+++ /dev/null
@@ -1,125 +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.Framework.Graphics.UserInterface;
-using osu.Framework.Threading;
-using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Rulesets;
-using osuTK;
-
-namespace osu.Game.Screens.OnlinePlay.Lounge.Components
-{
-    public abstract class FilterControl : CompositeDrawable
-    {
-        protected readonly FillFlowContainer Filters;
-
-        [Resolved(CanBeNull = true)]
-        private Bindable<FilterCriteria> filter { get; set; }
-
-        [Resolved]
-        private IBindable<RulesetInfo> ruleset { get; set; }
-
-        private readonly SearchTextBox search;
-        private readonly Dropdown<RoomStatusFilter> statusDropdown;
-
-        protected FilterControl()
-        {
-            RelativeSizeAxes = Axes.X;
-            Height = 70;
-
-            InternalChild = new FillFlowContainer
-            {
-                RelativeSizeAxes = Axes.Both,
-                Direction = FillDirection.Vertical,
-                Spacing = new Vector2(10),
-                Children = new Drawable[]
-                {
-                    search = new FilterSearchTextBox
-                    {
-                        Anchor = Anchor.TopRight,
-                        Origin = Anchor.TopRight,
-                        RelativeSizeAxes = Axes.X,
-                        Width = 0.6f,
-                    },
-                    Filters = new FillFlowContainer
-                    {
-                        Anchor = Anchor.TopRight,
-                        Origin = Anchor.TopRight,
-                        AutoSizeAxes = Axes.Both,
-                        Direction = FillDirection.Horizontal,
-                        Spacing = new Vector2(10),
-                        Child = statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
-                        {
-                            Anchor = Anchor.TopRight,
-                            Origin = Anchor.TopRight,
-                            RelativeSizeAxes = Axes.None,
-                            Width = 160,
-                        }
-                    },
-                }
-            };
-        }
-
-        [BackgroundDependencyLoader]
-        private void load(OsuColour colours)
-        {
-            filter ??= new Bindable<FilterCriteria>();
-        }
-
-        protected override void LoadComplete()
-        {
-            base.LoadComplete();
-
-            search.Current.BindValueChanged(_ => updateFilterDebounced());
-            ruleset.BindValueChanged(_ => UpdateFilter());
-            statusDropdown.Current.BindValueChanged(_ => UpdateFilter(), true);
-        }
-
-        private ScheduledDelegate scheduledFilterUpdate;
-
-        private void updateFilterDebounced()
-        {
-            scheduledFilterUpdate?.Cancel();
-            scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200);
-        }
-
-        protected void UpdateFilter() => Scheduler.AddOnce(updateFilter);
-
-        private void updateFilter()
-        {
-            scheduledFilterUpdate?.Cancel();
-
-            var criteria = CreateCriteria();
-            criteria.SearchString = search.Current.Value;
-            criteria.Status = statusDropdown.Current.Value;
-            criteria.Ruleset = ruleset.Value;
-
-            filter.Value = criteria;
-        }
-
-        protected virtual FilterCriteria CreateCriteria() => new FilterCriteria();
-
-        public bool HoldFocus
-        {
-            get => search.HoldFocus;
-            set => search.HoldFocus = value;
-        }
-
-        public void TakeFocus() => search.TakeFocus();
-
-        private class FilterSearchTextBox : SearchTextBox
-        {
-            [BackgroundDependencyLoader]
-            private void load()
-            {
-                BackgroundUnfocused = OsuColour.Gray(0.06f);
-                BackgroundFocused = OsuColour.Gray(0.12f);
-            }
-        }
-    }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs
deleted file mode 100644
index bbf34d3893..0000000000
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs
+++ /dev/null
@@ -1,57 +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.UserInterface;
-using osu.Game.Graphics.UserInterface;
-
-namespace osu.Game.Screens.OnlinePlay.Lounge.Components
-{
-    public class PlaylistsFilterControl : FilterControl
-    {
-        private readonly Dropdown<PlaylistsCategory> categoryDropdown;
-
-        public PlaylistsFilterControl()
-        {
-            Filters.Add(categoryDropdown = new SlimEnumDropdown<PlaylistsCategory>
-            {
-                Anchor = Anchor.TopRight,
-                Origin = Anchor.TopRight,
-                RelativeSizeAxes = Axes.None,
-                Width = 160,
-            });
-        }
-
-        protected override void LoadComplete()
-        {
-            base.LoadComplete();
-
-            categoryDropdown.Current.BindValueChanged(_ => UpdateFilter());
-        }
-
-        protected override FilterCriteria CreateCriteria()
-        {
-            var criteria = base.CreateCriteria();
-
-            switch (categoryDropdown.Current.Value)
-            {
-                case PlaylistsCategory.Normal:
-                    criteria.Category = "normal";
-                    break;
-
-                case PlaylistsCategory.Spotlight:
-                    criteria.Category = "spotlight";
-                    break;
-            }
-
-            return criteria;
-        }
-
-        private enum PlaylistsCategory
-        {
-            Any,
-            Normal,
-            Spotlight
-        }
-    }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 5e5863c7c4..46d9850fde 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -140,7 +140,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
 
                 roomFlow.Remove(toRemove);
 
-                selectedRoom.Value = null;
+                // selection may have a lease due to being in a sub screen.
+                if (!selectedRoom.Disabled)
+                    selectedRoom.Value = null;
             }
         }
 
@@ -152,7 +154,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
 
         protected override bool OnClick(ClickEvent e)
         {
-            selectedRoom.Value = null;
+            if (!selectedRoom.Disabled)
+                selectedRoom.Value = null;
             return base.OnClick(e);
         }
 
@@ -214,6 +217,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
 
         private void selectNext(int direction)
         {
+            if (selectedRoom.Disabled)
+                return;
+
             var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent);
 
             Room room;
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
index 122b30b1d2..e0e5cc415e 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs
@@ -2,6 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Diagnostics;
+using System.Collections.Generic;
 using System.Linq;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;
@@ -9,18 +11,20 @@ using osu.Framework.Bindables;
 using osu.Framework.Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input.Events;
 using osu.Framework.Screens;
+using osu.Framework.Threading;
+using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.Rooms;
 using osu.Game.Overlays;
+using osu.Game.Rulesets;
 using osu.Game.Screens.OnlinePlay.Lounge.Components;
 using osu.Game.Screens.OnlinePlay.Match;
 using osu.Game.Users;
 using osuTK;
-using osuTK.Graphics;
 
 namespace osu.Game.Screens.OnlinePlay.Lounge
 {
@@ -41,7 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
         private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
         private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
 
-        private FilterControl filter;
         private LoadingLayer loadingLayer;
 
         [Resolved]
@@ -53,31 +56,37 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
         [Resolved(CanBeNull = true)]
         private OngoingOperationTracker ongoingOperationTracker { get; set; }
 
+        [Resolved(CanBeNull = true)]
+        private Bindable<FilterCriteria> filter { get; set; }
+
+        [Resolved]
+        private IBindable<RulesetInfo> ruleset { get; set; }
+
         [CanBeNull]
         private IDisposable joiningRoomOperation { get; set; }
 
         private RoomsContainer roomsContainer;
+        private SearchTextBox searchTextBox;
+        private Dropdown<RoomStatusFilter> statusDropdown;
+
+        [CanBeNull]
+        private LeasedBindable<Room> selectionLease;
 
         [BackgroundDependencyLoader]
         private void load()
         {
+            filter ??= new Bindable<FilterCriteria>(new FilterCriteria());
+
             OsuScrollContainer scrollContainer;
 
-            InternalChildren = new Drawable[]
+            InternalChildren = new[]
             {
-                new Box
-                {
-                    RelativeSizeAxes = Axes.X,
-                    Height = 100,
-                    Colour = Color4.Black,
-                    Alpha = 0.5f,
-                },
+                loadingLayer = new LoadingLayer(true),
                 new Container
                 {
                     RelativeSizeAxes = Axes.Both,
                     Padding = new MarginPadding
                     {
-                        Top = 20,
                         Left = WaveOverlayContainer.WIDTH_PADDING,
                         Right = WaveOverlayContainer.WIDTH_PADDING,
                     },
@@ -86,26 +95,50 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
                         RelativeSizeAxes = Axes.Both,
                         RowDimensions = new[]
                         {
-                            new Dimension(GridSizeMode.AutoSize),
+                            new Dimension(GridSizeMode.Absolute, Header.HEIGHT),
+                            new Dimension(GridSizeMode.Absolute, 25),
                             new Dimension(GridSizeMode.Absolute, 20)
                         },
                         Content = new[]
                         {
+                            new Drawable[]
+                            {
+                                searchTextBox = new LoungeSearchTextBox
+                                {
+                                    Anchor = Anchor.CentreRight,
+                                    Origin = Anchor.CentreRight,
+                                    RelativeSizeAxes = Axes.X,
+                                    Width = 0.6f,
+                                },
+                            },
                             new Drawable[]
                             {
                                 new Container
                                 {
-                                    RelativeSizeAxes = Axes.X,
-                                    Height = 70,
-                                    Depth = -1,
+                                    RelativeSizeAxes = Axes.Both,
+                                    Depth = float.MinValue, // Contained filters should appear over the top of rooms.
                                     Children = new Drawable[]
                                     {
-                                        filter = CreateFilterControl(),
                                         Buttons.WithChild(CreateNewRoomButton().With(d =>
                                         {
-                                            d.Size = new Vector2(150, 25);
+                                            d.Anchor = Anchor.BottomLeft;
+                                            d.Origin = Anchor.BottomLeft;
+                                            d.Size = new Vector2(150, 37.5f);
                                             d.Action = () => Open();
-                                        }))
+                                        })),
+                                        new FillFlowContainer
+                                        {
+                                            Anchor = Anchor.TopRight,
+                                            Origin = Anchor.TopRight,
+                                            AutoSizeAxes = Axes.Both,
+                                            Direction = FillDirection.Horizontal,
+                                            Spacing = new Vector2(10),
+                                            ChildrenEnumerable = CreateFilterControls().Select(f => f.With(d =>
+                                            {
+                                                d.Anchor = Anchor.TopRight;
+                                                d.Origin = Anchor.TopRight;
+                                            }))
+                                        }
                                     }
                                 }
                             },
@@ -123,13 +156,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
                                             ScrollbarOverlapsContent = false,
                                             Child = roomsContainer = new RoomsContainer()
                                         },
-                                        loadingLayer = new LoadingLayer(true),
                                     }
                                 },
                             }
                         }
                     },
-                }
+                },
             };
 
             // scroll selected room into view on selection.
@@ -145,6 +177,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
         {
             base.LoadComplete();
 
+            searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced());
+            ruleset.BindValueChanged(_ => UpdateFilter());
+
             initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived);
             initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer());
 
@@ -153,13 +188,50 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
                 operationInProgress.BindTo(ongoingOperationTracker.InProgress);
                 operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
             }
+
+            updateFilter();
         }
 
-        protected override void OnFocus(FocusEvent e)
+        #region Filtering
+
+        protected void UpdateFilter() => Scheduler.AddOnce(updateFilter);
+
+        private ScheduledDelegate scheduledFilterUpdate;
+
+        private void updateFilterDebounced()
         {
-            filter.TakeFocus();
+            scheduledFilterUpdate?.Cancel();
+            scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200);
         }
 
+        private void updateFilter()
+        {
+            scheduledFilterUpdate?.Cancel();
+            filter.Value = CreateFilterCriteria();
+        }
+
+        protected virtual FilterCriteria CreateFilterCriteria() => new FilterCriteria
+        {
+            SearchString = searchTextBox.Current.Value,
+            Ruleset = ruleset.Value,
+            Status = statusDropdown.Current.Value
+        };
+
+        protected virtual IEnumerable<Drawable> CreateFilterControls()
+        {
+            statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
+            {
+                RelativeSizeAxes = Axes.None,
+                Width = 160,
+            };
+
+            statusDropdown.Current.BindValueChanged(_ => UpdateFilter());
+
+            yield return statusDropdown;
+        }
+
+        #endregion
+
         public override void OnEntering(IScreen last)
         {
             base.OnEntering(last);
@@ -171,6 +243,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
         {
             base.OnResuming(last);
 
+            Debug.Assert(selectionLease != null);
+
+            selectionLease.Return();
+            selectionLease = null;
+
             if (selectedRoom.Value?.RoomID.Value == null)
                 selectedRoom.Value = new Room();
 
@@ -191,14 +268,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
             base.OnSuspending(next);
         }
 
+        protected override void OnFocus(FocusEvent e)
+        {
+            searchTextBox.TakeFocus();
+        }
+
         private void onReturning()
         {
-            filter.HoldFocus = true;
+            searchTextBox.HoldFocus = true;
         }
 
         private void onLeaving()
         {
-            filter.HoldFocus = false;
+            searchTextBox.HoldFocus = false;
 
             // ensure any password prompt is dismissed.
             this.HidePopover();
@@ -238,13 +320,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
 
         protected virtual void OpenNewRoom(Room room)
         {
-            selectedRoom.Value = room;
+            selectionLease = selectedRoom.BeginLease(false);
+            Debug.Assert(selectionLease != null);
+            selectionLease.Value = room;
 
             this.Push(CreateRoomSubScreen(room));
         }
 
-        protected abstract FilterControl CreateFilterControl();
-
         protected abstract OsuButton CreateNewRoomButton();
 
         /// <summary>
@@ -262,5 +344,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
             else
                 loadingLayer.Hide();
         }
+
+        private class LoungeSearchTextBox : SearchTextBox
+        {
+            [BackgroundDependencyLoader]
+            private void load()
+            {
+                BackgroundUnfocused = OsuColour.Gray(0.06f);
+                BackgroundFocused = OsuColour.Gray(0.12f);
+            }
+        }
     }
 }
diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
index 2616abf825..243d2abf74 100644
--- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
@@ -8,8 +8,10 @@ using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
 using osu.Framework.Screens;
 using osu.Game.Audio;
 using osu.Game.Beatmaps;
@@ -62,8 +64,15 @@ namespace osu.Game.Screens.OnlinePlay.Match
 
         protected RoomSubScreen()
         {
+            Padding = new MarginPadding { Top = Header.HEIGHT };
+
             AddRangeInternal(new Drawable[]
             {
+                new Box
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Colour = Color4Extensions.FromHex(@"3e3a44") // This is super temporary.
+                },
                 BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
                 {
                     SelectedItem = { BindTarget = SelectedItem }
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs
deleted file mode 100644
index 37e0fd109a..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs
+++ /dev/null
@@ -1,17 +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.Game.Screens.OnlinePlay.Lounge.Components;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer
-{
-    public class MultiplayerFilterControl : FilterControl
-    {
-        protected override FilterCriteria CreateCriteria()
-        {
-            var criteria = base.CreateCriteria();
-            criteria.Category = "realtime";
-            return criteria;
-        }
-    }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
index 621ff8881f..ad7882abc2 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs
@@ -21,7 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
         [Resolved]
         private MultiplayerClient client { get; set; }
 
-        protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl();
+        protected override FilterCriteria CreateFilterCriteria()
+        {
+            var criteria = base.CreateFilterCriteria();
+            criteria.Category = @"realtime";
+            return criteria;
+        }
 
         protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton();
 
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index a8e44dd56c..1943ff668f 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
             isConnected.BindValueChanged(connected =>
             {
                 if (!connected.NewValue)
-                    Schedule(this.Exit);
+                    handleRoomLost();
             }, true);
 
             currentRoom.BindValueChanged(room =>
@@ -284,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
                     // the room has gone away.
                     // this could mean something happened during the join process, or an external connection issue occurred.
                     // one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97)
-                    Schedule(this.Exit);
+                    handleRoomLost();
                 }
             }, true);
         }
@@ -448,9 +448,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
 
         private void onRoomUpdated()
         {
+            // may happen if the client is kicked or otherwise removed from the room.
+            if (client.Room == null)
+            {
+                handleRoomLost();
+                return;
+            }
+
             Scheduler.AddOnce(UpdateMods);
         }
 
+        private void handleRoomLost() => Schedule(() =>
+        {
+            if (this.IsCurrentScreen())
+                this.Exit();
+            else
+                ValidForResume = false;
+        });
+
         private void onLoadRequested()
         {
             if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 3ba7b8b982..ca1a3710ab 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -181,7 +181,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
         protected override ResultsScreen CreateResults(ScoreInfo score)
         {
             Debug.Assert(RoomId.Value != null);
-            return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
+            return leaderboard.TeamScores.Count == 2
+                ? new MultiplayerTeamResultsScreen(score, RoomId.Value.Value, PlaylistItem, leaderboard.TeamScores)
+                : new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
         }
 
         protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs
new file mode 100644
index 0000000000..14a779dedf
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerTeamResultsScreen.cs
@@ -0,0 +1,152 @@
+// 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;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Localisation;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer
+{
+    public class MultiplayerTeamResultsScreen : MultiplayerResultsScreen
+    {
+        private readonly SortedDictionary<int, BindableInt> teamScores;
+
+        private Container winnerBackground;
+        private Drawable winnerText;
+
+        public MultiplayerTeamResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, SortedDictionary<int, BindableInt> teamScores)
+            : base(score, roomId, playlistItem)
+        {
+            if (teamScores.Count != 2)
+                throw new NotSupportedException(@"This screen currently only supports 2 teams");
+
+            this.teamScores = teamScores;
+        }
+
+        [Resolved]
+        private OsuColour colours { get; set; }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
+            const float winner_background_half_height = 250;
+
+            VerticalScrollContent.Anchor = VerticalScrollContent.Origin = Anchor.TopCentre;
+            VerticalScrollContent.Scale = new Vector2(0.9f);
+            VerticalScrollContent.Y = 75;
+
+            var redScore = teamScores.First().Value;
+            var blueScore = teamScores.Last().Value;
+
+            LocalisableString winner;
+            Colour4 winnerColour;
+
+            int comparison = redScore.Value.CompareTo(blueScore.Value);
+
+            if (comparison < 0)
+            {
+                // team name should eventually be coming from the multiplayer match state.
+                winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Blue");
+                winnerColour = colours.TeamColourBlue;
+            }
+            else if (comparison > 0)
+            {
+                // team name should eventually be coming from the multiplayer match state.
+                winner = MultiplayerTeamResultsScreenStrings.TeamWins(@"Red");
+                winnerColour = colours.TeamColourRed;
+            }
+            else
+            {
+                winner = MultiplayerTeamResultsScreenStrings.TheTeamsAreTied;
+                winnerColour = Colour4.White.Opacity(0.5f);
+            }
+
+            AddRangeInternal(new Drawable[]
+            {
+                new MatchScoreDisplay
+                {
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
+                    Team1Score = { BindTarget = redScore },
+                    Team2Score = { BindTarget = blueScore },
+                },
+                winnerBackground = new Container
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Alpha = 0,
+                    Children = new[]
+                    {
+                        new Box
+                        {
+                            RelativeSizeAxes = Axes.X,
+                            Height = winner_background_half_height,
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.BottomCentre,
+                            Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0), Colour4.Black.Opacity(0.4f))
+                        },
+                        new Box
+                        {
+                            RelativeSizeAxes = Axes.X,
+                            Height = winner_background_half_height,
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.TopCentre,
+                            Colour = ColourInfo.GradientVertical(Colour4.Black.Opacity(0.4f), Colour4.Black.Opacity(0))
+                        }
+                    }
+                },
+                (winnerText = new OsuSpriteText
+                {
+                    Alpha = 0,
+                    Font = OsuFont.Torus.With(size: 80, weight: FontWeight.Bold),
+                    Text = winner,
+                    Blending = BlendingParameters.Additive
+                }).WithEffect(new GlowEffect
+                {
+                    Colour = winnerColour,
+                }).With(e =>
+                {
+                    e.Anchor = Anchor.Centre;
+                    e.Origin = Anchor.Centre;
+                })
+            });
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            using (BeginDelayedSequence(300))
+            {
+                const double fade_in_duration = 600;
+
+                winnerText.FadeInFromZero(fade_in_duration, Easing.InQuint);
+                winnerBackground.FadeInFromZero(fade_in_duration, Easing.InQuint);
+
+                winnerText
+                    .ScaleTo(10)
+                    .ScaleTo(1, 600, Easing.InQuad)
+                    .Then()
+                    .ScaleTo(1.02f, 1600, Easing.OutQuint)
+                    .FadeOut(5000, Easing.InQuad);
+                winnerBackground.Delay(2200).FadeOut(2000);
+            }
+        }
+    }
+}
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 86ce61f845..fd265e9978 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Logging;
 using osu.Framework.Screens;
+using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics.Containers;
 using osu.Game.Input;
@@ -21,8 +22,9 @@ using osu.Game.Screens.Menu;
 using osu.Game.Screens.OnlinePlay.Components;
 using osu.Game.Screens.OnlinePlay.Lounge;
 using osu.Game.Screens.OnlinePlay.Lounge.Components;
-using osu.Game.Screens.OnlinePlay.Match;
 using osu.Game.Users;
+using osuTK;
+using osuTK.Graphics;
 
 namespace osu.Game.Screens.OnlinePlay
 {
@@ -71,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay
         [Resolved(CanBeNull = true)]
         private OsuLogo logo { get; set; }
 
-        private Drawable header;
-        private Drawable headerBackground;
-
         protected OnlinePlayScreen()
         {
             Anchor = Anchor.Centre;
@@ -104,42 +103,21 @@ namespace osu.Game.Screens.OnlinePlay
                     new Container
                     {
                         RelativeSizeAxes = Axes.Both,
-                        Padding = new MarginPadding { Top = Header.HEIGHT },
-                        Children = new[]
+                        Children = new Drawable[]
                         {
-                            header = new Container
+                            new BeatmapBackgroundSprite
                             {
-                                RelativeSizeAxes = Axes.X,
-                                Height = 400,
-                                Children = new[]
-                                {
-                                    headerBackground = new Container
-                                    {
-                                        RelativeSizeAxes = Axes.Both,
-                                        Width = 1.25f,
-                                        Masking = true,
-                                        Children = new Drawable[]
-                                        {
-                                            new HeaderBackgroundSprite
-                                            {
-                                                RelativeSizeAxes = Axes.X,
-                                                Height = 400 // Keep a static height so the header doesn't change as it's resized between subscreens
-                                            },
-                                        }
-                                    },
-                                    new Container
-                                    {
-                                        RelativeSizeAxes = Axes.Both,
-                                        Padding = new MarginPadding { Bottom = -1 }, // 1px padding to avoid a 1px gap due to masking
-                                        Child = new Box
-                                        {
-                                            RelativeSizeAxes = Axes.Both,
-                                            Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour)
-                                        },
-                                    }
-                                }
+                                RelativeSizeAxes = Axes.Both
                             },
-                            screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }
+                            new Box
+                            {
+                                RelativeSizeAxes = Axes.Both,
+                                Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f))
+                            },
+                            screenStack = new OnlinePlaySubScreenStack
+                            {
+                                RelativeSizeAxes = Axes.Both
+                            }
                         }
                     },
                     new Header(ScreenTitle, screenStack),
@@ -292,19 +270,6 @@ namespace osu.Game.Screens.OnlinePlay
 
         private void subScreenChanged(IScreen lastScreen, IScreen newScreen)
         {
-            switch (newScreen)
-            {
-                case LoungeSubScreen _:
-                    header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
-                    headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
-                    break;
-
-                case RoomSubScreen _:
-                    header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
-                    headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
-                    break;
-            }
-
             if (lastScreen is IOsuScreen lastOsuScreen)
                 Activity.UnbindFrom(lastOsuScreen.Activity);
 
@@ -335,13 +300,48 @@ namespace osu.Game.Screens.OnlinePlay
             }
         }
 
-        private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite
+        private class BeatmapBackgroundSprite : OnlinePlayBackgroundSprite
         {
-            protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both };
+            protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BlurredBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
 
-            private class BackgroundSprite : UpdateableBeatmapBackgroundSprite
+            public class BlurredBackgroundSprite : UpdateableBeatmapBackgroundSprite
             {
-                protected override double TransformDuration => 200;
+                public BlurredBackgroundSprite(BeatmapSetCoverType type)
+                    : base(type)
+                {
+                }
+
+                protected override double LoadDelay => 200;
+
+                protected override Drawable CreateDrawable(BeatmapInfo model) =>
+                    new BufferedLoader(base.CreateDrawable(model));
+            }
+
+            // This class is an unfortunate requirement due to `LongRunningLoad` requiring direct async loading.
+            // It means that if the web request fetching the beatmap background takes too long, it will suddenly appear.
+            internal class BufferedLoader : BufferedContainer
+            {
+                private readonly Drawable drawable;
+
+                public BufferedLoader(Drawable drawable)
+                {
+                    this.drawable = drawable;
+
+                    RelativeSizeAxes = Axes.Both;
+                    BlurSigma = new Vector2(10);
+                    FrameBufferScale = new Vector2(0.5f);
+                    CacheDrawnFrameBuffer = true;
+                }
+
+                [BackgroundDependencyLoader]
+                private void load()
+                {
+                    LoadComponentAsync(drawable, d =>
+                    {
+                        Add(d);
+                        ForceRedraw();
+                    });
+                }
             }
         }
 
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
index be28de5c43..1502463022 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs
@@ -59,6 +59,8 @@ namespace osu.Game.Screens.OnlinePlay
         [BackgroundDependencyLoader]
         private void load()
         {
+            LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
+
             initialBeatmap = Beatmap.Value;
             initialRuleset = Ruleset.Value;
             initialMods = Mods.Value.ToList();
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
index 4db1d6380d..eee4d4f407 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs
@@ -1,7 +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 System.Collections.Generic;
+using System.Linq;
 using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.API;
 using osu.Game.Online.Rooms;
@@ -16,7 +20,38 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
         [Resolved]
         private IAPIProvider api { get; set; }
 
-        protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl();
+        private Dropdown<PlaylistsCategory> categoryDropdown;
+
+        protected override IEnumerable<Drawable> CreateFilterControls()
+        {
+            categoryDropdown = new SlimEnumDropdown<PlaylistsCategory>
+            {
+                RelativeSizeAxes = Axes.None,
+                Width = 160,
+            };
+
+            categoryDropdown.Current.BindValueChanged(_ => UpdateFilter());
+
+            return base.CreateFilterControls().Append(categoryDropdown);
+        }
+
+        protected override FilterCriteria CreateFilterCriteria()
+        {
+            var criteria = base.CreateFilterCriteria();
+
+            switch (categoryDropdown.Current.Value)
+            {
+                case PlaylistsCategory.Normal:
+                    criteria.Category = @"normal";
+                    break;
+
+                case PlaylistsCategory.Spotlight:
+                    criteria.Category = @"spotlight";
+                    break;
+            }
+
+            return criteria;
+        }
 
         protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
 
@@ -30,5 +65,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
         }
 
         protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);
+
+        private enum PlaylistsCategory
+        {
+            Any,
+            Normal,
+            Spotlight
+        }
     }
 }
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index 63cb4f89f5..871555e5a3 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -6,29 +6,58 @@ using System.Linq;
 using JetBrains.Annotations;
 using osu.Framework.Bindables;
 using osu.Framework.Caching;
+using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
 using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
 using osu.Game.Users;
 using osuTK;
+using osuTK.Graphics;
 
 namespace osu.Game.Screens.Play.HUD
 {
-    public class GameplayLeaderboard : FillFlowContainer<GameplayLeaderboardScore>
+    public class GameplayLeaderboard : CompositeDrawable
     {
+        private readonly int maxPanels;
         private readonly Cached sorting = new Cached();
 
         public Bindable<bool> Expanded = new Bindable<bool>();
 
-        public GameplayLeaderboard()
+        protected readonly FillFlowContainer<GameplayLeaderboardScore> Flow;
+
+        private bool requiresScroll;
+        private readonly OsuScrollContainer scroll;
+
+        private GameplayLeaderboardScore trackedScore;
+
+        /// <summary>
+        /// Create a new leaderboard.
+        /// </summary>
+        /// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
+        public GameplayLeaderboard(int maxPanels = 8)
         {
+            this.maxPanels = maxPanels;
+
             Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
 
-            Direction = FillDirection.Vertical;
-
-            Spacing = new Vector2(2.5f);
-
-            LayoutDuration = 250;
-            LayoutEasing = Easing.OutQuint;
+            InternalChildren = new Drawable[]
+            {
+                scroll = new InputDisabledScrollContainer
+                {
+                    RelativeSizeAxes = Axes.Both,
+                    Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
+                    {
+                        RelativeSizeAxes = Axes.X,
+                        X = GameplayLeaderboardScore.SHEAR_WIDTH,
+                        AutoSizeAxes = Axes.Y,
+                        Direction = FillDirection.Vertical,
+                        Spacing = new Vector2(2.5f),
+                        LayoutDuration = 450,
+                        LayoutEasing = Easing.OutQuint,
+                    }
+                }
+            };
         }
 
         protected override void LoadComplete()
@@ -46,26 +75,87 @@ namespace osu.Game.Screens.Play.HUD
         /// Whether the player should be tracked on the leaderboard.
         /// Set to <c>true</c> for the local player or a player whose replay is currently being played.
         /// </param>
-        public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
+        public ILeaderboardScore Add([CanBeNull] User user, bool isTracked)
         {
             var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
 
+            if (isTracked)
+            {
+                if (trackedScore != null)
+                    throw new InvalidOperationException("Cannot track more than one score.");
+
+                trackedScore = drawable;
+            }
+
             drawable.Expanded.BindTo(Expanded);
 
-            base.Add(drawable);
+            Flow.Add(drawable);
             drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
 
-            Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y);
+            int displayCount = Math.Min(Flow.Count, maxPanels);
+            Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
+            requiresScroll = displayCount != Flow.Count;
 
             return drawable;
         }
 
+        public void Clear()
+        {
+            Flow.Clear();
+            trackedScore = null;
+            scroll.ScrollToStart(false);
+        }
+
         protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) =>
             new GameplayLeaderboardScore(user, isTracked);
 
-        public sealed override void Add(GameplayLeaderboardScore drawable)
+        protected override void Update()
         {
-            throw new NotSupportedException($"Use {nameof(AddPlayer)} instead.");
+            base.Update();
+
+            if (requiresScroll && trackedScore != null)
+            {
+                float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
+                scroll.ScrollTo(scrollTarget, false);
+            }
+
+            const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
+
+            float fadeBottom = scroll.Current + scroll.DrawHeight;
+            float fadeTop = scroll.Current + panel_height;
+
+            if (scroll.Current <= 0) fadeTop -= panel_height;
+            if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
+
+            // logic is mostly shared with Leaderboard, copied here for simplicity.
+            foreach (var c in Flow.Children)
+            {
+                float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, Flow).Y;
+                float bottomY = topY + panel_height;
+
+                bool requireTopFade = requiresScroll && topY <= fadeTop;
+                bool requireBottomFade = requiresScroll && bottomY >= fadeBottom;
+
+                if (!requireTopFade && !requireBottomFade)
+                    c.Colour = Color4.White;
+                else if (topY > fadeBottom + panel_height || bottomY < fadeTop - panel_height)
+                    c.Colour = Color4.Transparent;
+                else
+                {
+                    if (requireBottomFade)
+                    {
+                        c.Colour = ColourInfo.GradientVertical(
+                            Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / panel_height, 1)),
+                            Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / panel_height, 1)));
+                    }
+                    else if (requiresScroll)
+                    {
+                        c.Colour = ColourInfo.GradientVertical(
+                            Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / panel_height, 1)),
+                            Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / panel_height, 1)));
+                    }
+                }
+            }
         }
 
         private void sort()
@@ -73,15 +163,26 @@ namespace osu.Game.Screens.Play.HUD
             if (sorting.IsValid)
                 return;
 
-            var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList();
+            var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
 
-            for (int i = 0; i < Count; i++)
+            for (int i = 0; i < Flow.Count; i++)
             {
-                SetLayoutPosition(orderedByScore[i], i);
+                Flow.SetLayoutPosition(orderedByScore[i], i);
                 orderedByScore[i].ScorePosition = i + 1;
             }
 
             sorting.Validate();
         }
+
+        private class InputDisabledScrollContainer : OsuScrollContainer
+        {
+            public InputDisabledScrollContainer()
+            {
+                ScrollbarVisible = false;
+            }
+
+            public override bool HandlePositionalInput => false;
+            public override bool HandleNonPositionalInput => false;
+        }
     }
 }
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 433bf78e9b..85cf9d1966 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -81,7 +81,10 @@ namespace osu.Game.Screens.Play.HUD
         [CanBeNull]
         public User User { get; }
 
-        private readonly bool trackedPlayer;
+        /// <summary>
+        /// Whether this score is the local user or a replay player (and should be focused / always visible).
+        /// </summary>
+        public readonly bool Tracked;
 
         private Container mainFillContainer;
 
@@ -97,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD
         /// Creates a new <see cref="GameplayLeaderboardScore"/>.
         /// </summary>
         /// <param name="user">The score's player.</param>
-        /// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param>
-        public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer)
+        /// <param name="tracked">Whether the player is the local user or a replay player.</param>
+        public GameplayLeaderboardScore([CanBeNull] User user, bool tracked)
         {
             User = user;
-            this.trackedPlayer = trackedPlayer;
+            Tracked = tracked;
 
             AutoSizeAxes = Axes.X;
             Height = PANEL_HEIGHT;
@@ -338,7 +341,7 @@ namespace osu.Game.Screens.Play.HUD
                 panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
                 textColour = TextColour ?? Color4.White;
             }
-            else if (trackedPlayer)
+            else if (Tracked)
             {
                 widthExtension = true;
                 panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
index c77b872786..68e3f0df7d 100644
--- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
 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.Sprites;
 using osu.Game.Graphics.UserInterface;
@@ -104,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
             base.LoadComplete();
 
             Team1Score.BindValueChanged(_ => updateScores());
-            Team2Score.BindValueChanged(_ => updateScores());
+            Team2Score.BindValueChanged(_ => updateScores(), true);
         }
 
         private void updateScores()
@@ -171,6 +172,8 @@ namespace osu.Game.Screens.Play.HUD
                 => displayedSpriteText.Font = winning
                     ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
                     : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
+
+            protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0");
         }
     }
 }
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index 3f9258930e..19cb6aeb50 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD
 
                     var trackedUser = UserScores[user.Id];
 
-                    var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id);
+                    var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
                     leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
                     leaderboardScore.TotalScore.BindTo(trackedUser.Score);
                     leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
@@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD
                     continue;
 
                 if (TeamScores.TryGetValue(u.Team.Value, out var team))
-                    team.Value += (int)u.Score.Value;
+                    team.Value += (int)Math.Round(u.Score.Value);
             }
         }
 
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 2cf2555b3e..13df9fefa7 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -57,8 +57,6 @@ namespace osu.Game.Screens.Play
 
         private Bindable<HUDVisibilityMode> configVisibilityMode;
 
-        private readonly Container visibilityContainer;
-
         private readonly BindableBool replayLoaded = new BindableBool();
 
         private static bool hasShownNotificationOnce;
@@ -72,7 +70,7 @@ namespace osu.Game.Screens.Play
 
         private readonly SkinnableTargetContainer mainComponents;
 
-        private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
+        private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
 
         public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
         {
@@ -84,13 +82,9 @@ namespace osu.Game.Screens.Play
             Children = new Drawable[]
             {
                 CreateFailingLayer(),
-                visibilityContainer = new Container
+                mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
                 {
                     RelativeSizeAxes = Axes.Both,
-                    Child = mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
-                    {
-                        RelativeSizeAxes = Axes.Both,
-                    },
                 },
                 topRightElements = new FillFlowContainer
                 {
diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs
index f28622f42e..6aa7e017ce 100644
--- a/osu.Game/Screens/Play/SongProgress.cs
+++ b/osu.Game/Screens/Play/SongProgress.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Screens.Play
                 Objects = drawableRuleset.Objects;
             }
 
-            config.BindWith(OsuSetting.ShowProgressGraph, ShowGraph);
+            config.BindWith(OsuSetting.ShowDifficultyGraph, ShowGraph);
 
             graph.FillColour = bar.FillColour = colours.BlueLighter;
         }
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index b458d7c17f..d44d1f2cc9 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Screens.Ranking
 
         protected ScorePanelList ScorePanelList { get; private set; }
 
+        protected VerticalScrollContainer VerticalScrollContent { get; private set; }
+
         [Resolved(CanBeNull = true)]
         private Player player { get; set; }
 
@@ -77,7 +79,7 @@ namespace osu.Game.Screens.Ranking
                 {
                     new Drawable[]
                     {
-                        new VerticalScrollContainer
+                        VerticalScrollContent = new VerticalScrollContainer
                         {
                             RelativeSizeAxes = Axes.Both,
                             ScrollbarVisible = false,
@@ -343,7 +345,7 @@ namespace osu.Game.Screens.Ranking
         {
         }
 
-        private class VerticalScrollContainer : OsuScrollContainer
+        protected class VerticalScrollContainer : OsuScrollContainer
         {
             protected override Container<Drawable> Content => content;
 
@@ -351,6 +353,8 @@ namespace osu.Game.Screens.Ranking
 
             public VerticalScrollContainer()
             {
+                Masking = false;
+
                 base.Content.Add(content = new Container { RelativeSizeAxes = Axes.X });
             }
 
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index 3779523094..5b4e077100 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Screens.Select
 {
     public class BeatmapInfoWedge : VisibilityContainer
     {
+        public const float BORDER_THICKNESS = 2.5f;
         private const float shear_width = 36.75f;
 
         private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
@@ -59,7 +60,7 @@ namespace osu.Game.Screens.Select
             Shear = wedged_container_shear;
             Masking = true;
             BorderColour = new Color4(221, 255, 255, 255);
-            BorderThickness = 2.5f;
+            BorderThickness = BORDER_THICKNESS;
             Alpha = 0;
             EdgeEffect = new EdgeEffectParameters
             {
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index b9e912df8e..f47bc5f466 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Screens.Select
             set
             {
                 searchText = value;
-                SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
+                SearchTerms = searchText.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToArray();
 
                 SearchNumber = null;
 
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 270addc8e6..bb3df0d4e0 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -79,6 +79,8 @@ namespace osu.Game.Screens.Select
 
         protected BeatmapCarousel Carousel { get; private set; }
 
+        protected Container LeftArea { get; private set; }
+
         private BeatmapInfoWedge beatmapInfoWedge;
         private DialogOverlay dialogOverlay;
 
@@ -186,12 +188,12 @@ namespace osu.Game.Screens.Select
                             {
                                 new Drawable[]
                                 {
-                                    new Container
+                                    LeftArea = new Container
                                     {
                                         Origin = Anchor.BottomLeft,
                                         Anchor = Anchor.BottomLeft,
                                         RelativeSizeAxes = Axes.Both,
-
+                                        Padding = new MarginPadding { Top = left_area_padding },
                                         Children = new Drawable[]
                                         {
                                             beatmapInfoWedge = new BeatmapInfoWedge
@@ -200,8 +202,8 @@ namespace osu.Game.Screens.Select
                                                 RelativeSizeAxes = Axes.X,
                                                 Margin = new MarginPadding
                                                 {
-                                                    Top = left_area_padding,
                                                     Right = left_area_padding,
+                                                    Left = -BeatmapInfoWedge.BORDER_THICKNESS, // Hide the left border
                                                 },
                                             },
                                             new Container
@@ -210,7 +212,7 @@ namespace osu.Game.Screens.Select
                                                 Padding = new MarginPadding
                                                 {
                                                     Bottom = Footer.HEIGHT,
-                                                    Top = WEDGE_HEIGHT + left_area_padding,
+                                                    Top = WEDGE_HEIGHT,
                                                     Left = left_area_padding,
                                                     Right = left_area_padding * 2,
                                                 },
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
index a28b4140ca..67b79d7390 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs
@@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
         {
             Debug.Assert(Room != null);
 
-            return ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.UserID == userId));
+            return ((IMultiplayerClient)this).UserKicked(Room.Users.Single(u => u.UserID == userId));
         }
 
         public override async Task ChangeSettings(MultiplayerRoomSettings settings)
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 9ee1ad167e..d4dba9330f 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -37,7 +37,7 @@
     </PackageReference>
     <PackageReference Include="Realm" Version="10.3.0" />
     <PackageReference Include="ppy.osu.Framework" Version="2021.813.0" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
     <PackageReference Include="Sentry" Version="3.8.3" />
     <PackageReference Include="SharpCompress" Version="0.28.3" />
     <PackageReference Include="NUnit" Version="3.13.2" />
diff --git a/osu.iOS.props b/osu.iOS.props
index c378f24b69..7e514afe74 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -71,7 +71,7 @@
   </ItemGroup>
   <ItemGroup Label="Package References">
     <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.813.0" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
   </ItemGroup>
   <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
   <PropertyGroup>