diff --git a/README.md b/README.md
index abddb1faa1..04f133fd56 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ This project is still heavily under development, but is in a state where users a
 
 We are accepting bug reports (please report with as much detail as possible). Feature requests are welcome as long as you read and understand the contribution guidelines listed below.
 
+Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh/home/changelog).
+
 ## Requirements
 
 - A desktop platform with the [.NET Core SDK 2.2](https://www.microsoft.com/net/learn/get-started) or higher installed.
@@ -20,17 +22,24 @@ We are accepting bug reports (please report with as much detail as possible). Fe
 
 ### Releases
 
-If you are not interested in developing the game, please head over to the [releases](https://github.com/ppy/osu/releases) to download a precompiled build with automatic updating enabled.
+![](https://puu.sh/DCmvA/f6a74f5fbb.png)
 
-- Windows (x64) users should download and run `install.exe`.
-- macOS users (10.12 "Sierra" and higher) should download and run `osu.app.zip`.
-- iOS users can join the [TestFlight beta program](https://t.co/xQJmHkfC18).
+If you are not interested in developing the game, you can consume our [binary releases](https://github.com/ppy/osu/releases).
+
+**Latest build:***
+
+| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe)  | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) |
+| ------------- | ------------- |
+
+- **Linux** users are recommended to self-compile until we have official deployment in place.
+- **iOS** users can join the [TestFlight beta program](https://t.co/xQJmHkfC18) (note that due to high demand this is regularly full).
+- **Android** users can self-compile, and expect a public beta soon.
 
 If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
 
 ### Downloading the source code
 
-Clone the repository **including submodules**:
+Clone the repository:
 
 ```shell
 git clone https://github.com/ppy/osu
@@ -45,7 +54,7 @@ git pull
 
 ### Building
 
-Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided below.
+Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this provided [below](#contributing).
 
 > Visual Studio Code users must run the `Restore` task before any build attempt.
 
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
index 25e1aebd18..33b3667c4f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
@@ -2,13 +2,10 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Collections.Generic;
-using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Lines;
-using osu.Framework.Graphics.Primitives;
 using osuTK;
 using osuTK.Graphics;
-using osuTK.Graphics.ES30;
 
 namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
 {
@@ -19,8 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
         private readonly SliderPath path;
         protected Path Path => path;
 
-        private readonly BufferedContainer container;
-
         public float PathRadius
         {
             get => path.PathRadius;
@@ -44,8 +39,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
                     return;
 
                 path.AccentColour = value;
-
-                container.ForceRedraw();
             }
         }
 
@@ -61,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
                     return;
 
                 path.BorderColour = value;
-
-                container.ForceRedraw();
             }
         }
 
@@ -78,23 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
                     return;
 
                 path.BorderSize = value;
-
-                container.ForceRedraw();
             }
         }
 
-        public Quad PathDrawQuad => container.ScreenSpaceDrawQuad;
-
         protected SliderBody()
         {
-            InternalChild = container = new BufferedContainer
-            {
-                RelativeSizeAxes = Axes.Both,
-                CacheDrawnFrameBuffer = true,
-                Child = path = new SliderPath { Blending = BlendingMode.None }
-            };
-
-            container.Attach(RenderbufferInternalFormat.DepthComponent16);
+            InternalChild = path = new SliderPath();
         }
 
         public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos);
@@ -103,11 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
         /// Sets the vertices of the path which should be drawn by this <see cref="SliderBody"/>.
         /// </summary>
         /// <param name="vertices">The vertices</param>
-        protected void SetVertices(IReadOnlyList<Vector2> vertices)
-        {
-            path.Vertices = vertices;
-            container.ForceRedraw();
-        }
+        protected void SetVertices(IReadOnlyList<Vector2> vertices) => path.Vertices = vertices;
 
         private class SliderPath : SmoothPath
         {
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 1b8fa0de01..2276b9f9f4 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -54,7 +54,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
 
             for (int i = 0; i < max_sprites; i++)
             {
-                parts[i].InvalidationID = 0;
+                // InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node
+                // This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms
+                parts[i].InvalidationID = 1;
                 parts[i].WasUpdated = true;
             }
         }
@@ -210,7 +212,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
                         Vector2 pos = parts[i].Position;
                         float localTime = parts[i].Time;
 
-                        texture.DrawQuad(
+                        DrawQuad(
+                            texture,
                             new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y),
                             DrawColourInfo.Colour,
                             null,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
new file mode 100644
index 0000000000..4878587dcd
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+    public class TestSceneFailAnimation : AllPlayersTestScene
+    {
+        protected override Player CreatePlayer(Ruleset ruleset)
+        {
+            Mods.Value = Array.Empty<Mod>();
+            return new FailPlayer();
+        }
+
+        public override IReadOnlyList<Type> RequiredTypes => new[]
+        {
+            typeof(AllPlayersTestScene),
+            typeof(TestPlayer),
+            typeof(Player),
+        };
+
+        protected override void AddCheckSteps()
+        {
+            AddUntilStep("wait for fail", () => Player.HasFailed);
+            AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State == Visibility.Visible);
+        }
+
+        private class FailPlayer : TestPlayer
+        {
+            public new FailOverlay FailOverlay => base.FailOverlay;
+
+            public FailPlayer()
+                : base(false, false)
+            {
+            }
+
+            protected override void LoadComplete()
+            {
+                base.LoadComplete();
+                ScoreProcessor.FailConditions += _ => true;
+            }
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index b6f8638f4a..12e91df77c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         public void TestPauseAfterFail()
         {
             AddUntilStep("wait for fail", () => Player.HasFailed);
-            AddAssert("fail overlay shown", () => Player.FailOverlayVisible);
+            AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible);
 
             confirmClockRunning(false);
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index 6815018be6..2f88a4b01d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Online
                             FlagName = @"TH",
                         },
                     },
-                    Rank = ScoreRank.F,
+                    Rank = ScoreRank.D,
                     PP = 160,
                     MaxCombo = 1234,
                     TotalScore = 123456,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs
index 3d75470328..9365e2c5b1 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs
@@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 },
                 new ScoreInfo
                 {
-                    Rank = ScoreRank.F,
+                    Rank = ScoreRank.D,
                     Accuracy = 0.6025,
                     MaxCombo = 244,
                     TotalScore = 1707827,
@@ -206,7 +206,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 },
                 new ScoreInfo
                 {
-                    Rank = ScoreRank.F,
+                    Rank = ScoreRank.D,
                     Accuracy = 0.5140,
                     MaxCombo = 244,
                     TotalScore = 1707827,
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 },
                 new ScoreInfo
                 {
-                    Rank = ScoreRank.F,
+                    Rank = ScoreRank.D,
                     Accuracy = 0.4222,
                     MaxCombo = 244,
                     TotalScore = 1707827,
diff --git a/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs b/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs
new file mode 100644
index 0000000000..096ac951de
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/OsuGridTestScene.cs
@@ -0,0 +1,50 @@
+// 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;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+    /// <summary>
+    /// An abstract test case which exposes small cells arranged in a grid.
+    /// Useful for displaying multiple configurations of a tested component at a glance.
+    /// </summary>
+    public abstract class OsuGridTestScene : OsuTestScene
+    {
+        private readonly Drawable[,] cells;
+
+        /// <summary>
+        /// The amount of rows in the grid.
+        /// </summary>
+        protected readonly int Rows;
+
+        /// <summary>
+        /// The amount of columns in the grid.
+        /// </summary>
+        protected readonly int Cols;
+
+        /// <summary>
+        /// Constructs a grid test case with the given dimensions.
+        /// </summary>
+        protected OsuGridTestScene(int rows, int cols)
+        {
+            Rows = rows;
+            Cols = cols;
+
+            GridContainer testContainer;
+            Add(testContainer = new GridContainer { RelativeSizeAxes = Axes.Both });
+
+            cells = new Drawable[rows, cols];
+            for (int r = 0; r < rows; r++)
+            for (int c = 0; c < cols; c++)
+                cells[r, c] = new Container { RelativeSizeAxes = Axes.Both };
+
+            testContainer.Content = cells.ToJagged();
+        }
+
+        protected Container Cell(int index) => (Container)cells[index / Cols, index % Cols];
+        protected Container Cell(int row, int col) => (Container)cells[row, col];
+    }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
index 590ee4e720..8fe31b7ad6 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
@@ -84,7 +84,6 @@ namespace osu.Game.Tests.Visual.UserInterface
             testLocalCursor();
             testUserCursorOverride();
             testMultipleLocalCursors();
-            ReturnUserInput();
         }
 
         /// <summary>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs
index b9a6d74f19..b0233d35f9 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingAnimation.cs
@@ -3,13 +3,12 @@
 
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Testing;
 using osu.Game.Graphics.UserInterface;
 using osuTK.Graphics;
 
 namespace osu.Game.Tests.Visual.UserInterface
 {
-    public class TestSceneLoadingAnimation : GridTestScene //todo: this should be an OsuTestScene
+    public class TestSceneLoadingAnimation : OsuGridTestScene
     {
         public TestSceneLoadingAnimation()
             : base(2, 2)
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs
new file mode 100644
index 0000000000..6a41d08f01
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuAnimatedButton.cs
@@ -0,0 +1,72 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+    public class TestSceneOsuAnimatedButton : OsuGridTestScene
+    {
+        public TestSceneOsuAnimatedButton()
+            : base(3, 2)
+        {
+            Cell(0).Add(new BaseContainer("relative sized")
+            {
+                RelativeSizeAxes = Axes.Both,
+            });
+
+            Cell(1).Add(new BaseContainer("auto sized")
+            {
+                AutoSizeAxes = Axes.Both
+            });
+
+            Cell(2).Add(new BaseContainer("relative Y auto X")
+            {
+                RelativeSizeAxes = Axes.Y,
+                AutoSizeAxes = Axes.X
+            });
+
+            Cell(3).Add(new BaseContainer("relative X auto Y")
+            {
+                RelativeSizeAxes = Axes.X,
+                AutoSizeAxes = Axes.Y
+            });
+
+            Cell(4).Add(new BaseContainer("fixed")
+            {
+                Size = new Vector2(100),
+            });
+
+            Cell(5).Add(new BaseContainer("fixed")
+            {
+                Size = new Vector2(100, 50),
+            });
+
+            AddToggleStep("toggle enabled", toggle =>
+            {
+                for (int i = 0; i < 6; i++)
+                    ((BaseContainer)Cell(i).Child).Action = toggle ? () => { } : (Action)null;
+            });
+        }
+
+        public class BaseContainer : OsuAnimatedButton
+        {
+            public BaseContainer(string text)
+            {
+                Anchor = Anchor.Centre;
+                Origin = Anchor.Centre;
+
+                Add(new OsuSpriteText
+                {
+                    Anchor = Anchor.Centre,
+                    Origin = Anchor.Centre,
+                    Text = text
+                });
+            }
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs
new file mode 100644
index 0000000000..dbef7d1686
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs
@@ -0,0 +1,194 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+    [TestFixture]
+    public class TestSceneOsuHoverContainer : ManualInputManagerTestScene
+    {
+        private OsuHoverTestContainer hoverContainer;
+        private Box colourContainer;
+
+        [SetUp]
+        public void SetUp() => Schedule(() =>
+        {
+            Child = hoverContainer = new OsuHoverTestContainer
+            {
+                Enabled = { Value = true },
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                Size = new Vector2(100),
+                Child = colourContainer = new Box
+                {
+                    RelativeSizeAxes = Axes.Both,
+                },
+            };
+
+            doMoveOut();
+        });
+
+        [Description("Checks IsHovered property value on a container when it is hovered/unhovered.")]
+        [TestCase(true, TestName = "Enabled_Check_IsHovered")]
+        [TestCase(false, TestName = "Disabled_Check_IsHovered")]
+        public void TestIsHoveredHasProperValue(bool isEnabled)
+        {
+            setContainerEnabledTo(isEnabled);
+
+            checkNotHovered();
+
+            moveToText();
+            checkHovered();
+
+            moveOut();
+            checkNotHovered();
+
+            moveToText();
+            checkHovered();
+
+            moveOut();
+            checkNotHovered();
+        }
+
+        [Test]
+        [Description("Checks colour fading on an enabled container when it is hovered/unhovered.")]
+        public void TestTransitionWhileEnabled()
+        {
+            enableContainer();
+
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+
+            moveOut();
+            waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+
+            moveOut();
+            waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR);
+        }
+
+        [Test]
+        [Description("Checks colour fading on a disabled container when it is hovered/unhovered.")]
+        public void TestNoTransitionWhileDisabled()
+        {
+            disableContainer();
+
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveOut();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveOut();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+        }
+
+        [Test]
+        [Description("Checks that when a disabled & hovered container gets enabled, colour fading happens")]
+        public void TestBecomesEnabledTransition()
+        {
+            disableContainer();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            enableContainer();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+        }
+
+        [Test]
+        [Description("Checks that when an enabled & hovered container gets disabled, colour fading happens")]
+        public void TestBecomesDisabledTransition()
+        {
+            enableContainer();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+
+            disableContainer();
+            waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR);
+        }
+
+        [Test]
+        [Description("Checks that when a hovered container gets enabled and disabled multiple times, colour fading happens")]
+        public void TestDisabledChangesMultipleTimes()
+        {
+            enableContainer();
+            checkColour(OsuHoverTestContainer.IDLE_COLOUR);
+
+            moveToText();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+
+            disableContainer();
+            waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR);
+
+            enableContainer();
+            waitUntilColourIs(OsuHoverTestContainer.HOVER_COLOUR);
+
+            disableContainer();
+            waitUntilColourIs(OsuHoverTestContainer.IDLE_COLOUR);
+        }
+
+        private void enableContainer() => setContainerEnabledTo(true);
+
+        private void disableContainer() => setContainerEnabledTo(false);
+
+        private void setContainerEnabledTo(bool newValue)
+        {
+            string word = newValue ? "Enable" : "Disable";
+            AddStep($"{word} container", () => hoverContainer.Enabled.Value = newValue);
+        }
+
+        private void moveToText() => AddStep("Move mouse to text", () => InputManager.MoveMouseTo(hoverContainer));
+
+        private void moveOut() => AddStep("Move out", doMoveOut);
+
+        private void checkHovered() => AddAssert("Check hovered", () => hoverContainer.IsHovered);
+
+        private void checkNotHovered() => AddAssert("Check not hovered", () => !hoverContainer.IsHovered);
+
+        private void checkColour(ColourInfo expectedColour)
+            => AddAssert($"Check colour to be '{expectedColour}'", () => currentColour.Equals(expectedColour));
+
+        private void waitUntilColourIs(ColourInfo expectedColour)
+            => AddUntilStep($"Wait until hover colour is {expectedColour}", () => currentColour.Equals(expectedColour));
+
+        private ColourInfo currentColour => colourContainer.DrawColourInfo.Colour;
+
+        /// <summary>
+        ///     Moves the cursor to top left corner of the screen
+        /// </summary>
+        private void doMoveOut()
+            => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.TopLeft.X, InputManager.ScreenSpaceDrawQuad.TopLeft.Y));
+
+        private sealed class OsuHoverTestContainer : OsuHoverContainer
+        {
+            public static readonly Color4 HOVER_COLOUR = Color4.Red;
+            public static readonly Color4 IDLE_COLOUR = Color4.Green;
+
+            public OsuHoverTestContainer()
+            {
+                HoverColour = HOVER_COLOUR;
+                IdleColour = IDLE_COLOUR;
+            }
+        }
+    }
+}
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index d479483508..6e162ca95e 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -1,6 +1,10 @@
 // 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.IO;
+using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
@@ -12,22 +16,24 @@ using osu.Game.Beatmaps;
 
 namespace osu.Game.Audio
 {
-    /// <summary>
-    /// A central store for the retrieval of <see cref="PreviewTrack"/>s.
-    /// </summary>
     public class PreviewTrackManager : Component
     {
         private readonly BindableDouble muteBindable = new BindableDouble();
 
         private AudioManager audio;
-        private ITrackStore trackStore;
+        private PreviewTrackStore trackStore;
 
         private TrackManagerPreviewTrack current;
 
         [BackgroundDependencyLoader]
         private void load(AudioManager audio, FrameworkConfigManager config)
         {
-            trackStore = audio.GetTrackStore(new OnlineStore());
+            // this is a temporary solution to get around muting ourselves.
+            // todo: update this once we have a BackgroundTrackManager or similar.
+            trackStore = new PreviewTrackStore(new OnlineStore());
+
+            audio.AddItem(trackStore);
+            trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack);
 
             this.audio = audio;
 
@@ -103,5 +109,46 @@ namespace osu.Game.Audio
 
             protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3");
         }
+
+        private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
+        {
+            private readonly IResourceStore<byte[]> store;
+
+            internal PreviewTrackStore(IResourceStore<byte[]> store)
+            {
+                this.store = store;
+            }
+
+            public Track GetVirtual(double length = double.PositiveInfinity)
+            {
+                if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
+
+                var track = new TrackVirtual(length);
+                AddItem(track);
+                return track;
+            }
+
+            public Track Get(string name)
+            {
+                if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
+
+                if (string.IsNullOrEmpty(name)) return null;
+
+                var dataStream = store.GetStream(name);
+
+                if (dataStream == null)
+                    return null;
+
+                Track track = new TrackBass(dataStream);
+                AddItem(track);
+                return track;
+            }
+
+            public Task<Track> GetAsync(string name) => Task.Run(() => Get(name));
+
+            public Stream GetStream(string name) => store.GetStream(name);
+
+            public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
+        }
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 0200dd44ac..b6fe7f88fa 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -342,6 +342,7 @@ namespace osu.Game.Beatmaps
                 OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
                 Beatmaps = new List<BeatmapInfo>(),
                 Metadata = beatmap.Metadata,
+                DateAdded = DateTimeOffset.UtcNow
             };
         }
 
diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs
index e111f77ba1..390236e053 100644
--- a/osu.Game/Beatmaps/BeatmapSetInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations.Schema;
 using System.Linq;
@@ -20,6 +21,8 @@ namespace osu.Game.Beatmaps
             set => onlineBeatmapSetID = value > 0 ? value : null;
         }
 
+        public DateTimeOffset DateAdded { get; set; }
+
         public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
 
         public BeatmapMetadata Metadata { get; set; }
diff --git a/osu.Game/Configuration/DatabasedSetting.cs b/osu.Game/Configuration/DatabasedSetting.cs
index 3e0a9ecd28..f5c92b3029 100644
--- a/osu.Game/Configuration/DatabasedSetting.cs
+++ b/osu.Game/Configuration/DatabasedSetting.cs
@@ -15,6 +15,8 @@ namespace osu.Game.Configuration
 
         public int? Variant { get; set; }
 
+        public int? SkinInfoID { get; set; }
+
         [Column("Key")]
         public string Key { get; set; }
 
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index e2c7693700..29113e0e2f 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -214,7 +214,6 @@ namespace osu.Game.Graphics.Backgrounds
                 base.Draw(vertexAction);
 
                 shader.Bind();
-                texture.TextureGL.Bind();
 
                 Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy;
 
@@ -231,7 +230,8 @@ namespace osu.Game.Graphics.Backgrounds
                     ColourInfo colourInfo = DrawColourInfo.Colour;
                     colourInfo.ApplyChild(particle.Colour);
 
-                    texture.DrawTriangle(
+                    DrawTriangle(
+                        texture,
                         triangle,
                         colourInfo,
                         null,
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 6dbe340efb..1f31e4cdda 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Graphics.Containers
         {
             if (AutoSizeAxes != Axes.None)
             {
-                content.RelativeSizeAxes = RelativeSizeAxes;
+                content.RelativeSizeAxes = (Axes.Both & ~AutoSizeAxes);
                 content.AutoSizeAxes = AutoSizeAxes;
             }
 
diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
index eb2d926424..67af79c763 100644
--- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
@@ -24,8 +24,13 @@ namespace osu.Game.Graphics.Containers
         {
             Enabled.ValueChanged += e =>
             {
-                if (!e.NewValue)
-                    unhover();
+                if (isHovered)
+                {
+                    if (e.NewValue)
+                        fadeIn();
+                    else
+                        fadeOut();
+                }
             };
         }
 
@@ -33,28 +38,28 @@ namespace osu.Game.Graphics.Containers
 
         protected override bool OnHover(HoverEvent e)
         {
+            if (isHovered)
+                return false;
+
+            isHovered = true;
+
             if (!Enabled.Value)
                 return false;
 
-            EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint));
-            isHovered = true;
+            fadeIn();
 
             return base.OnHover(e);
         }
 
         protected override void OnHoverLost(HoverLostEvent e)
-        {
-            unhover();
-            base.OnHoverLost(e);
-        }
-
-        private void unhover()
         {
             if (!isHovered)
                 return;
 
             isHovered = false;
-            EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint));
+            fadeOut();
+
+            base.OnHoverLost(e);
         }
 
         [BackgroundDependencyLoader]
@@ -69,5 +74,9 @@ namespace osu.Game.Graphics.Containers
             base.LoadComplete();
             EffectTargets.ForEach(d => d.FadeColour(IdleColour));
         }
+
+        private void fadeIn() => EffectTargets.ForEach(d => d.FadeColour(HoverColour, FADE_DURATION, Easing.OutQuint));
+
+        private void fadeOut() => EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint));
     }
 }
diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs
index 6414e488e8..052e9194fa 100644
--- a/osu.Game/Graphics/UserInterface/IconButton.cs
+++ b/osu.Game/Graphics/UserInterface/IconButton.cs
@@ -66,6 +66,7 @@ namespace osu.Game.Graphics.UserInterface
             set
             {
                 Content.RelativeSizeAxes = Axes.None;
+                Content.AutoSizeAxes = Axes.None;
                 Content.Size = value;
             }
         }
diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
index a8041c79fc..236b72766f 100644
--- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
@@ -74,6 +74,12 @@ namespace osu.Game.Graphics.UserInterface
         [BackgroundDependencyLoader]
         private void load(OsuColour colours)
         {
+            if (AutoSizeAxes != Axes.None)
+            {
+                content.RelativeSizeAxes = (Axes.Both & ~AutoSizeAxes);
+                content.AutoSizeAxes = AutoSizeAxes;
+            }
+
             Enabled.BindValueChanged(enabled => this.FadeColour(enabled.NewValue ? Color4.White : colours.Gray9, 200, Easing.OutQuint), true);
         }
 
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
new file mode 100644
index 0000000000..348c42adb9
--- /dev/null
+++ b/osu.Game/Migrations/20190525060824_SkinSettings.Designer.cs
@@ -0,0 +1,498 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using osu.Game.Database;
+
+namespace osu.Game.Migrations
+{
+    [DbContext(typeof(OsuDbContext))]
+    [Migration("20190525060824_SkinSettings")]
+    partial class SkinSettings
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<float>("ApproachRate");
+
+                    b.Property<float>("CircleSize");
+
+                    b.Property<float>("DrainRate");
+
+                    b.Property<float>("OverallDifficulty");
+
+                    b.Property<double>("SliderMultiplier");
+
+                    b.Property<double>("SliderTickRate");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("BeatmapDifficulty");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("AudioLeadIn");
+
+                    b.Property<int>("BaseDifficultyID");
+
+                    b.Property<int>("BeatDivisor");
+
+                    b.Property<int>("BeatmapSetInfoID");
+
+                    b.Property<bool>("Countdown");
+
+                    b.Property<double>("DistanceSpacing");
+
+                    b.Property<int>("GridSize");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<bool>("Hidden");
+
+                    b.Property<bool>("LetterboxInBreaks");
+
+                    b.Property<string>("MD5Hash");
+
+                    b.Property<int?>("MetadataID");
+
+                    b.Property<int?>("OnlineBeatmapID");
+
+                    b.Property<string>("Path");
+
+                    b.Property<int>("RulesetID");
+
+                    b.Property<bool>("SpecialStyle");
+
+                    b.Property<float>("StackLeniency");
+
+                    b.Property<double>("StarDifficulty");
+
+                    b.Property<int>("Status");
+
+                    b.Property<string>("StoredBookmarks");
+
+                    b.Property<double>("TimelineZoom");
+
+                    b.Property<string>("Version");
+
+                    b.Property<bool>("WidescreenStoryboard");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BaseDifficultyID");
+
+                    b.HasIndex("BeatmapSetInfoID");
+
+                    b.HasIndex("Hash");
+
+                    b.HasIndex("MD5Hash");
+
+                    b.HasIndex("MetadataID");
+
+                    b.HasIndex("OnlineBeatmapID")
+                        .IsUnique();
+
+                    b.HasIndex("RulesetID");
+
+                    b.ToTable("BeatmapInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Artist");
+
+                    b.Property<string>("ArtistUnicode");
+
+                    b.Property<string>("AudioFile");
+
+                    b.Property<string>("AuthorString")
+                        .HasColumnName("Author");
+
+                    b.Property<string>("BackgroundFile");
+
+                    b.Property<int>("PreviewTime");
+
+                    b.Property<string>("Source");
+
+                    b.Property<string>("Tags");
+
+                    b.Property<string>("Title");
+
+                    b.Property<string>("TitleUnicode");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("BeatmapMetadata");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("BeatmapSetInfoID");
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BeatmapSetInfoID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.ToTable("BeatmapSetFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int?>("MetadataID");
+
+                    b.Property<int?>("OnlineBeatmapSetID");
+
+                    b.Property<bool>("Protected");
+
+                    b.Property<int>("Status");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("DeletePending");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.HasIndex("MetadataID");
+
+                    b.HasIndex("OnlineBeatmapSetID")
+                        .IsUnique();
+
+                    b.ToTable("BeatmapSetInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Key")
+                        .HasColumnName("Key");
+
+                    b.Property<int?>("RulesetID");
+
+                    b.Property<int?>("SkinInfoID");
+
+                    b.Property<string>("StringValue")
+                        .HasColumnName("Value");
+
+                    b.Property<int?>("Variant");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("SkinInfoID");
+
+                    b.HasIndex("RulesetID", "Variant");
+
+                    b.ToTable("Settings");
+                });
+
+            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int>("ReferenceCount");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.HasIndex("ReferenceCount");
+
+                    b.ToTable("FileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("IntAction")
+                        .HasColumnName("Action");
+
+                    b.Property<string>("KeysString")
+                        .HasColumnName("Keys");
+
+                    b.Property<int?>("RulesetID");
+
+                    b.Property<int?>("Variant");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("IntAction");
+
+                    b.HasIndex("RulesetID", "Variant");
+
+                    b.ToTable("KeyBinding");
+                });
+
+            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
+                {
+                    b.Property<int?>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<bool>("Available");
+
+                    b.Property<string>("InstantiationInfo");
+
+                    b.Property<string>("Name");
+
+                    b.Property<string>("ShortName");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Available");
+
+                    b.HasIndex("ShortName")
+                        .IsUnique();
+
+                    b.ToTable("RulesetInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.Property<int?>("ScoreInfoID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.HasIndex("ScoreInfoID");
+
+                    b.ToTable("ScoreFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<double>("Accuracy")
+                        .HasColumnType("DECIMAL(1,4)");
+
+                    b.Property<int>("BeatmapInfoID");
+
+                    b.Property<int>("Combo");
+
+                    b.Property<DateTimeOffset>("Date");
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int>("MaxCombo");
+
+                    b.Property<string>("ModsJson")
+                        .HasColumnName("Mods");
+
+                    b.Property<long?>("OnlineScoreID");
+
+                    b.Property<double?>("PP");
+
+                    b.Property<int>("Rank");
+
+                    b.Property<int>("RulesetID");
+
+                    b.Property<string>("StatisticsJson")
+                        .HasColumnName("Statistics");
+
+                    b.Property<long>("TotalScore");
+
+                    b.Property<long?>("UserID")
+                        .HasColumnName("UserID");
+
+                    b.Property<string>("UserString")
+                        .HasColumnName("User");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BeatmapInfoID");
+
+                    b.HasIndex("OnlineScoreID")
+                        .IsUnique();
+
+                    b.HasIndex("RulesetID");
+
+                    b.ToTable("ScoreInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.Property<int>("SkinInfoID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.HasIndex("SkinInfoID");
+
+                    b.ToTable("SkinFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Creator");
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<string>("Name");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("DeletePending");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.ToTable("SkinInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
+                        .WithMany()
+                        .HasForeignKey("BaseDifficultyID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
+                        .WithMany("Beatmaps")
+                        .HasForeignKey("BeatmapSetInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+                        .WithMany("Beatmaps")
+                        .HasForeignKey("MetadataID");
+
+                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+                        .WithMany()
+                        .HasForeignKey("RulesetID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("BeatmapSetInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+                        .WithMany("BeatmapSets")
+                        .HasForeignKey("MetadataID");
+                });
+
+            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+                {
+                    b.HasOne("osu.Game.Skinning.SkinInfo")
+                        .WithMany("Settings")
+                        .HasForeignKey("SkinInfoID");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Scoring.ScoreInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("ScoreInfoID");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
+                        .WithMany("Scores")
+                        .HasForeignKey("BeatmapInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+                        .WithMany()
+                        .HasForeignKey("RulesetID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Skinning.SkinInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("SkinInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs
new file mode 100644
index 0000000000..99237419b7
--- /dev/null
+++ b/osu.Game/Migrations/20190525060824_SkinSettings.cs
@@ -0,0 +1,54 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace osu.Game.Migrations
+{
+    public partial class SkinSettings : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.Sql(@"create table Settings_dg_tmp
+            (
+	            ID INTEGER not null
+		            constraint PK_Settings
+			            primary key autoincrement,
+	            Key TEXT not null,
+	            RulesetID INTEGER,
+	            Value TEXT,
+	            Variant INTEGER,
+	            SkinInfoID int
+		            constraint Settings_SkinInfo_ID_fk
+			            references SkinInfo
+				            on delete restrict
+            );
+
+            insert into Settings_dg_tmp(ID, Key, RulesetID, Value, Variant) select ID, Key, RulesetID, Value, Variant from Settings;
+
+            drop table Settings;
+
+            alter table Settings_dg_tmp rename to Settings;
+
+            create index IX_Settings_RulesetID_Variant
+	            on Settings (RulesetID, Variant);
+
+            create index Settings_SkinInfoID_index
+	            on Settings (SkinInfoID);
+
+            ");
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_Settings_SkinInfo_SkinInfoID",
+                table: "Settings");
+
+            migrationBuilder.DropIndex(
+                name: "IX_Settings_SkinInfoID",
+                table: "Settings");
+
+            migrationBuilder.DropColumn(
+                name: "SkinInfoID",
+                table: "Settings");
+        }
+    }
+}
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
new file mode 100644
index 0000000000..9477369aa0
--- /dev/null
+++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.Designer.cs
@@ -0,0 +1,489 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using osu.Game.Database;
+
+namespace osu.Game.Migrations
+{
+    [DbContext(typeof(OsuDbContext))]
+    [Migration("20190605091246_AddDateAddedColumnToBeatmapSet")]
+    partial class AddDateAddedColumnToBeatmapSet
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "2.2.4-servicing-10062");
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<float>("ApproachRate");
+
+                    b.Property<float>("CircleSize");
+
+                    b.Property<float>("DrainRate");
+
+                    b.Property<float>("OverallDifficulty");
+
+                    b.Property<double>("SliderMultiplier");
+
+                    b.Property<double>("SliderTickRate");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("BeatmapDifficulty");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("AudioLeadIn");
+
+                    b.Property<int>("BaseDifficultyID");
+
+                    b.Property<int>("BeatDivisor");
+
+                    b.Property<int>("BeatmapSetInfoID");
+
+                    b.Property<bool>("Countdown");
+
+                    b.Property<double>("DistanceSpacing");
+
+                    b.Property<int>("GridSize");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<bool>("Hidden");
+
+                    b.Property<bool>("LetterboxInBreaks");
+
+                    b.Property<string>("MD5Hash");
+
+                    b.Property<int?>("MetadataID");
+
+                    b.Property<int?>("OnlineBeatmapID");
+
+                    b.Property<string>("Path");
+
+                    b.Property<int>("RulesetID");
+
+                    b.Property<bool>("SpecialStyle");
+
+                    b.Property<float>("StackLeniency");
+
+                    b.Property<double>("StarDifficulty");
+
+                    b.Property<int>("Status");
+
+                    b.Property<string>("StoredBookmarks");
+
+                    b.Property<double>("TimelineZoom");
+
+                    b.Property<string>("Version");
+
+                    b.Property<bool>("WidescreenStoryboard");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BaseDifficultyID");
+
+                    b.HasIndex("BeatmapSetInfoID");
+
+                    b.HasIndex("Hash");
+
+                    b.HasIndex("MD5Hash");
+
+                    b.HasIndex("MetadataID");
+
+                    b.HasIndex("OnlineBeatmapID")
+                        .IsUnique();
+
+                    b.HasIndex("RulesetID");
+
+                    b.ToTable("BeatmapInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Artist");
+
+                    b.Property<string>("ArtistUnicode");
+
+                    b.Property<string>("AudioFile");
+
+                    b.Property<string>("AuthorString")
+                        .HasColumnName("Author");
+
+                    b.Property<string>("BackgroundFile");
+
+                    b.Property<int>("PreviewTime");
+
+                    b.Property<string>("Source");
+
+                    b.Property<string>("Tags");
+
+                    b.Property<string>("Title");
+
+                    b.Property<string>("TitleUnicode");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("BeatmapMetadata");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("BeatmapSetInfoID");
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BeatmapSetInfoID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.ToTable("BeatmapSetFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<DateTimeOffset>("DateAdded");
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int?>("MetadataID");
+
+                    b.Property<int?>("OnlineBeatmapSetID");
+
+                    b.Property<bool>("Protected");
+
+                    b.Property<int>("Status");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("DeletePending");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.HasIndex("MetadataID");
+
+                    b.HasIndex("OnlineBeatmapSetID")
+                        .IsUnique();
+
+                    b.ToTable("BeatmapSetInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Key")
+                        .HasColumnName("Key");
+
+                    b.Property<int?>("RulesetID");
+
+                    b.Property<string>("StringValue")
+                        .HasColumnName("Value");
+
+                    b.Property<int?>("Variant");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("RulesetID", "Variant");
+
+                    b.ToTable("Settings");
+                });
+
+            modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int>("ReferenceCount");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.HasIndex("ReferenceCount");
+
+                    b.ToTable("FileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("IntAction")
+                        .HasColumnName("Action");
+
+                    b.Property<string>("KeysString")
+                        .HasColumnName("Keys");
+
+                    b.Property<int?>("RulesetID");
+
+                    b.Property<int?>("Variant");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("IntAction");
+
+                    b.HasIndex("RulesetID", "Variant");
+
+                    b.ToTable("KeyBinding");
+                });
+
+            modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
+                {
+                    b.Property<int?>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<bool>("Available");
+
+                    b.Property<string>("InstantiationInfo");
+
+                    b.Property<string>("Name");
+
+                    b.Property<string>("ShortName");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Available");
+
+                    b.HasIndex("ShortName")
+                        .IsUnique();
+
+                    b.ToTable("RulesetInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.Property<int?>("ScoreInfoID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.HasIndex("ScoreInfoID");
+
+                    b.ToTable("ScoreFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<double>("Accuracy")
+                        .HasColumnType("DECIMAL(1,4)");
+
+                    b.Property<int>("BeatmapInfoID");
+
+                    b.Property<int>("Combo");
+
+                    b.Property<DateTimeOffset>("Date");
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<int>("MaxCombo");
+
+                    b.Property<string>("ModsJson")
+                        .HasColumnName("Mods");
+
+                    b.Property<long?>("OnlineScoreID");
+
+                    b.Property<double?>("PP");
+
+                    b.Property<int>("Rank");
+
+                    b.Property<int>("RulesetID");
+
+                    b.Property<string>("StatisticsJson")
+                        .HasColumnName("Statistics");
+
+                    b.Property<long>("TotalScore");
+
+                    b.Property<long?>("UserID")
+                        .HasColumnName("UserID");
+
+                    b.Property<string>("UserString")
+                        .HasColumnName("User");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BeatmapInfoID");
+
+                    b.HasIndex("OnlineScoreID")
+                        .IsUnique();
+
+                    b.HasIndex("RulesetID");
+
+                    b.ToTable("ScoreInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<int>("FileInfoID");
+
+                    b.Property<string>("Filename")
+                        .IsRequired();
+
+                    b.Property<int>("SkinInfoID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("FileInfoID");
+
+                    b.HasIndex("SkinInfoID");
+
+                    b.ToTable("SkinFileInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd();
+
+                    b.Property<string>("Creator");
+
+                    b.Property<bool>("DeletePending");
+
+                    b.Property<string>("Hash");
+
+                    b.Property<string>("Name");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("DeletePending");
+
+                    b.HasIndex("Hash")
+                        .IsUnique();
+
+                    b.ToTable("SkinInfo");
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
+                        .WithMany()
+                        .HasForeignKey("BaseDifficultyID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
+                        .WithMany("Beatmaps")
+                        .HasForeignKey("BeatmapSetInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+                        .WithMany("Beatmaps")
+                        .HasForeignKey("MetadataID");
+
+                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+                        .WithMany()
+                        .HasForeignKey("RulesetID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("BeatmapSetInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
+                        .WithMany("BeatmapSets")
+                        .HasForeignKey("MetadataID");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Scoring.ScoreInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("ScoreInfoID");
+                });
+
+            modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
+                {
+                    b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
+                        .WithMany("Scores")
+                        .HasForeignKey("BeatmapInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
+                        .WithMany()
+                        .HasForeignKey("RulesetID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
+                {
+                    b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
+                        .WithMany()
+                        .HasForeignKey("FileInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+
+                    b.HasOne("osu.Game.Skinning.SkinInfo")
+                        .WithMany("Files")
+                        .HasForeignKey("SkinInfoID")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
new file mode 100644
index 0000000000..55dc18b6a3
--- /dev/null
+++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs
@@ -0,0 +1,24 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace osu.Game.Migrations
+{
+    public partial class AddDateAddedColumnToBeatmapSet : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<DateTimeOffset>(
+                name: "DateAdded",
+                table: "BeatmapSetInfo",
+                nullable: false,
+                defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "DateAdded",
+                table: "BeatmapSetInfo");
+        }
+    }
+}
diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
index f942d357e8..11b032a941 100644
--- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
+++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs
@@ -166,6 +166,8 @@ namespace osu.Game.Migrations
                     b.Property<int>("ID")
                         .ValueGeneratedOnAdd();
 
+                    b.Property<DateTimeOffset>("DateAdded");
+
                     b.Property<bool>("DeletePending");
 
                     b.Property<string>("Hash");
@@ -203,6 +205,8 @@ namespace osu.Game.Migrations
 
                     b.Property<int?>("RulesetID");
 
+                    b.Property<int?>("SkinInfoID");
+
                     b.Property<string>("StringValue")
                         .HasColumnName("Value");
 
@@ -210,6 +214,8 @@ namespace osu.Game.Migrations
 
                     b.HasKey("ID");
 
+                    b.HasIndex("SkinInfoID");
+
                     b.HasIndex("RulesetID", "Variant");
 
                     b.ToTable("Settings");
@@ -442,6 +448,13 @@ namespace osu.Game.Migrations
                         .HasForeignKey("MetadataID");
                 });
 
+            modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
+                {
+                    b.HasOne("osu.Game.Skinning.SkinInfo")
+                        .WithMany("Settings")
+                        .HasForeignKey("SkinInfoID");
+                });
+
             modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
                 {
                     b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs
index abaff9b7ae..140e228acd 100644
--- a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Online.API.Requests.Responses
         public string MessageHtml { get; set; }
 
         [JsonProperty("major")]
-        public bool? Major { get; set; }
+        public bool Major { get; set; }
 
         [JsonProperty("created_at")]
         public DateTimeOffset? CreatedAt { get; set; }
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index 89da0fc254..cbcf3e6160 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
                         Text = "#1",
                         Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold, italics: true)
                     },
-                    rank = new DrawableRank(ScoreRank.F)
+                    rank = new DrawableRank(ScoreRank.D)
                     {
                         Anchor = Anchor.Centre,
                         Origin = Anchor.Centre,
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 57615332da..3d145af562 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -13,6 +13,7 @@ using System.Text.RegularExpressions;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Users;
 using osuTK.Graphics;
+using osu.Framework.Allocation;
 
 namespace osu.Game.Overlays.Changelog
 {
@@ -45,8 +46,12 @@ namespace osu.Game.Overlays.Changelog
                     Direction = FillDirection.Vertical,
                 },
             };
+        }
 
-            foreach (var categoryEntries in build.ChangelogEntries.GroupBy(b => b.Category).OrderBy(c => c.Key))
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            foreach (var categoryEntries in Build.ChangelogEntries.GroupBy(b => b.Category).OrderBy(c => c.Key))
             {
                 ChangelogEntries.Add(new OsuSpriteText
                 {
@@ -69,34 +74,69 @@ namespace osu.Game.Overlays.Changelog
                         Margin = new MarginPadding { Vertical = 5 },
                     };
 
+                    var entryColour = entry.Major ? colours.YellowLight : Color4.White;
+
                     title.AddIcon(FontAwesome.Solid.Check, t =>
                     {
                         t.Font = fontSmall;
+                        t.Colour = entryColour;
                         t.Padding = new MarginPadding { Left = -17, Right = 5 };
                     });
 
-                    title.AddText(entry.Title, t => { t.Font = fontLarge; });
+                    title.AddText(entry.Title, t =>
+                    {
+                        t.Font = fontLarge;
+                        t.Colour = entryColour;
+                    });
 
                     if (!string.IsNullOrEmpty(entry.Repository))
                     {
-                        title.AddText(" (", t => t.Font = fontLarge);
+                        title.AddText(" (", t =>
+                        {
+                            t.Font = fontLarge;
+                            t.Colour = entryColour;
+                        });
                         title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, Online.Chat.LinkAction.External,
-                            creationParameters: t => { t.Font = fontLarge; });
-                        title.AddText(")", t => t.Font = fontLarge);
+                            creationParameters: t =>
+                            {
+                                t.Font = fontLarge;
+                                t.Colour = entryColour;
+                            });
+                        title.AddText(")", t =>
+                        {
+                            t.Font = fontLarge;
+                            t.Colour = entryColour;
+                        });
                     }
 
-                    title.AddText(" by ", t => t.Font = fontMedium);
+                    title.AddText(" by ", t =>
+                    {
+                        t.Font = fontMedium;
+                        t.Colour = entryColour;
+                    });
 
                     if (entry.GithubUser.UserId != null)
                         title.AddUserLink(new User
                         {
                             Username = entry.GithubUser.OsuUsername,
                             Id = entry.GithubUser.UserId.Value
-                        }, t => t.Font = fontMedium);
+                        }, t =>
+                        {
+                            t.Font = fontMedium;
+                            t.Colour = entryColour;
+                        });
                     else if (entry.GithubUser.GithubUrl != null)
-                        title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t => t.Font = fontMedium);
+                        title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, Online.Chat.LinkAction.External, null, null, t =>
+                        {
+                            t.Font = fontMedium;
+                            t.Colour = entryColour;
+                        });
                     else
-                        title.AddText(entry.GithubUser.DisplayName, t => t.Font = fontSmall);
+                        title.AddText(entry.GithubUser.DisplayName, t =>
+                        {
+                            t.Font = fontSmall;
+                            t.Colour = entryColour;
+                        });
 
                     ChangelogEntries.Add(title);
 
diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs
index fb38ddcbd1..fdc6f096bc 100644
--- a/osu.Game/Overlays/HoldToConfirmOverlay.cs
+++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs
@@ -2,6 +2,8 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Shapes;
 using osu.Game.Graphics.Containers;
@@ -17,6 +19,11 @@ namespace osu.Game.Overlays
     {
         private Box overlay;
 
+        private readonly BindableDouble audioVolume = new BindableDouble(1);
+
+        [Resolved]
+        private AudioManager audio { get; set; }
+
         [BackgroundDependencyLoader]
         private void load()
         {
@@ -33,7 +40,19 @@ namespace osu.Game.Overlays
                 }
             };
 
-            Progress.ValueChanged += p => overlay.Alpha = (float)p.NewValue;
+            Progress.ValueChanged += p =>
+            {
+                audioVolume.Value = 1 - p.NewValue;
+                overlay.Alpha = (float)p.NewValue;
+            };
+
+            audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioVolume);
+        }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume);
+            base.Dispose(isDisposing);
         }
     }
 }
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index b675a35970..dec58f4c9e 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -1,40 +1,40 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osuTK;
-using osuTK.Graphics;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Backgrounds;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Mods;
 using System;
 using System.Collections.Generic;
 using System.Linq;
+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.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Backgrounds;
 using osu.Game.Graphics.Containers;
-using osu.Game.Rulesets;
+using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Overlays.Mods.Sections;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Screens;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
 
 namespace osu.Game.Overlays.Mods
 {
     public class ModSelectOverlay : WaveOverlayContainer
     {
-        private const float content_width = 0.8f;
-
-        protected Color4 LowMultiplierColour, HighMultiplierColour;
-
         protected readonly TriangleButton DeselectAllButton;
-        protected readonly OsuSpriteText MultiplierLabel, UnrankedLabel;
-        private readonly FillFlowContainer footerContainer;
+        protected readonly TriangleButton CloseButton;
+
+        protected readonly OsuSpriteText MultiplierLabel;
+        protected readonly OsuSpriteText UnrankedLabel;
 
         protected override bool BlockNonPositionalInput => false;
 
@@ -46,154 +46,14 @@ namespace osu.Game.Overlays.Mods
 
         protected readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
 
-        [BackgroundDependencyLoader(true)]
-        private void load(OsuColour colours, IBindable<RulesetInfo> ruleset, AudioManager audio, Bindable<IReadOnlyList<Mod>> mods)
-        {
-            LowMultiplierColour = colours.Red;
-            HighMultiplierColour = colours.Green;
-            UnrankedLabel.Colour = colours.Blue;
+        protected Color4 LowMultiplierColour;
+        protected Color4 HighMultiplierColour;
 
-            Ruleset.BindTo(ruleset);
-            if (mods != null) SelectedMods.BindTo(mods);
-
-            sampleOn = audio.Samples.Get(@"UI/check-on");
-            sampleOff = audio.Samples.Get(@"UI/check-off");
-        }
-
-        protected override void LoadComplete()
-        {
-            base.LoadComplete();
-
-            Ruleset.BindValueChanged(rulesetChanged, true);
-            SelectedMods.BindValueChanged(selectedModsChanged, true);
-        }
-
-        protected override void Dispose(bool isDisposing)
-        {
-            base.Dispose(isDisposing);
-
-            Ruleset.UnbindAll();
-            SelectedMods.UnbindAll();
-        }
-
-        private void rulesetChanged(ValueChangedEvent<RulesetInfo> e)
-        {
-            if (e.NewValue == null) return;
-
-            var instance = e.NewValue.CreateInstance();
-
-            foreach (ModSection section in ModSectionsContainer.Children)
-                section.Mods = instance.GetModsFor(section.ModType);
-
-            // attempt to re-select any already selected mods.
-            // this may be the first time we are receiving the ruleset, in which case they will still match.
-            selectedModsChanged(new ValueChangedEvent<IReadOnlyList<Mod>>(SelectedMods.Value, SelectedMods.Value));
-
-            // write the mods back to the SelectedMods bindable in the case a change was not applicable.
-            // this generally isn't required as the previous line will perform deselection; just here for safety.
-            refreshSelectedMods();
-        }
-
-        private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> e)
-        {
-            foreach (ModSection section in ModSectionsContainer.Children)
-                section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList());
-
-            updateMods();
-        }
-
-        private void updateMods()
-        {
-            double multiplier = 1.0;
-            bool ranked = true;
-
-            foreach (Mod mod in SelectedMods.Value)
-            {
-                multiplier *= mod.ScoreMultiplier;
-                ranked &= mod.Ranked;
-            }
-
-            MultiplierLabel.Text = $"{multiplier:N2}x";
-            if (multiplier > 1.0)
-                MultiplierLabel.FadeColour(HighMultiplierColour, 200);
-            else if (multiplier < 1.0)
-                MultiplierLabel.FadeColour(LowMultiplierColour, 200);
-            else
-                MultiplierLabel.FadeColour(Color4.White, 200);
-
-            UnrankedLabel.FadeTo(ranked ? 0 : 1, 200);
-        }
-
-        protected override void PopOut()
-        {
-            base.PopOut();
-
-            footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-            footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-
-            foreach (ModSection section in ModSectionsContainer.Children)
-            {
-                section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-                section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-                section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
-            }
-        }
-
-        protected override void PopIn()
-        {
-            base.PopIn();
-
-            footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-            footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-
-            foreach (ModSection section in ModSectionsContainer.Children)
-            {
-                section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-                section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-                section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
-            }
-        }
-
-        public void DeselectAll()
-        {
-            foreach (ModSection section in ModSectionsContainer.Children)
-                section.DeselectAll();
-
-            refreshSelectedMods();
-        }
-
-        /// <summary>
-        /// Deselect one or more mods.
-        /// </summary>
-        /// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
-        /// <param name="immediate">Set to true to bypass animations and update selections immediately.</param>
-        public void DeselectTypes(Type[] modTypes, bool immediate = false)
-        {
-            if (modTypes.Length == 0) return;
-
-            foreach (ModSection section in ModSectionsContainer.Children)
-                section.DeselectTypes(modTypes, immediate);
-        }
+        private const float content_width = 0.8f;
+        private readonly FillFlowContainer footerContainer;
 
         private SampleChannel sampleOn, sampleOff;
 
-        private void modButtonPressed(Mod selectedMod)
-        {
-            if (selectedMod != null)
-            {
-                if (State == Visibility.Visible) sampleOn?.Play();
-                DeselectTypes(selectedMod.IncompatibleMods, true);
-            }
-            else
-            {
-                if (State == Visibility.Visible) sampleOff?.Play();
-            }
-
-            refreshSelectedMods();
-        }
-
-        private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
-
         public ModSelectOverlay()
         {
             Waves.FirstWaveColour = OsuColour.FromHex(@"19b0e2");
@@ -364,6 +224,16 @@ namespace osu.Game.Overlays.Mods
                                                     Right = 20
                                                 }
                                             },
+                                            CloseButton = new TriangleButton
+                                            {
+                                                Width = 180,
+                                                Text = "Close",
+                                                Action = Hide,
+                                                Margin = new MarginPadding
+                                                {
+                                                    Right = 20
+                                                }
+                                            },
                                             new OsuSpriteText
                                             {
                                                 Text = @"Score Multiplier:",
@@ -401,5 +271,171 @@ namespace osu.Game.Overlays.Mods
                 },
             };
         }
+
+        [BackgroundDependencyLoader(true)]
+        private void load(OsuColour colours, IBindable<RulesetInfo> ruleset, AudioManager audio, Bindable<IReadOnlyList<Mod>> mods)
+        {
+            LowMultiplierColour = colours.Red;
+            HighMultiplierColour = colours.Green;
+            UnrankedLabel.Colour = colours.Blue;
+
+            Ruleset.BindTo(ruleset);
+            if (mods != null) SelectedMods.BindTo(mods);
+
+            sampleOn = audio.Samples.Get(@"UI/check-on");
+            sampleOff = audio.Samples.Get(@"UI/check-off");
+        }
+
+        public void DeselectAll()
+        {
+            foreach (var section in ModSectionsContainer.Children)
+                section.DeselectAll();
+
+            refreshSelectedMods();
+        }
+
+        /// <summary>
+        /// Deselect one or more mods.
+        /// </summary>
+        /// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
+        /// <param name="immediate">Set to true to bypass animations and update selections immediately.</param>
+        public void DeselectTypes(Type[] modTypes, bool immediate = false)
+        {
+            if (modTypes.Length == 0) return;
+
+            foreach (var section in ModSectionsContainer.Children)
+                section.DeselectTypes(modTypes, immediate);
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            Ruleset.BindValueChanged(rulesetChanged, true);
+            SelectedMods.BindValueChanged(selectedModsChanged, true);
+        }
+
+        protected override void PopOut()
+        {
+            base.PopOut();
+
+            footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+            footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+
+            foreach (var section in ModSectionsContainer.Children)
+            {
+                section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+                section.ButtonsContainer.MoveToX(100f, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+                section.ButtonsContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
+            }
+        }
+
+        protected override void PopIn()
+        {
+            base.PopIn();
+
+            footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
+            footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
+
+            foreach (var section in ModSectionsContainer.Children)
+            {
+                section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), WaveContainer.APPEAR_DURATION, Easing.OutQuint);
+                section.ButtonsContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint);
+                section.ButtonsContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint);
+            }
+        }
+
+        protected override bool OnKeyDown(KeyDownEvent e)
+        {
+            switch (e.Key)
+            {
+                case Key.Number1:
+                    DeselectAllButton.Click();
+                    return true;
+
+                case Key.Number2:
+                    CloseButton.Click();
+                    return true;
+            }
+
+            return base.OnKeyDown(e);
+        }
+
+        private void rulesetChanged(ValueChangedEvent<RulesetInfo> e)
+        {
+            if (e.NewValue == null) return;
+
+            var instance = e.NewValue.CreateInstance();
+
+            foreach (var section in ModSectionsContainer.Children)
+                section.Mods = instance.GetModsFor(section.ModType);
+
+            // attempt to re-select any already selected mods.
+            // this may be the first time we are receiving the ruleset, in which case they will still match.
+            selectedModsChanged(new ValueChangedEvent<IReadOnlyList<Mod>>(SelectedMods.Value, SelectedMods.Value));
+
+            // write the mods back to the SelectedMods bindable in the case a change was not applicable.
+            // this generally isn't required as the previous line will perform deselection; just here for safety.
+            refreshSelectedMods();
+        }
+
+        private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> e)
+        {
+            foreach (var section in ModSectionsContainer.Children)
+                section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList());
+
+            updateMods();
+        }
+
+        private void updateMods()
+        {
+            var multiplier = 1.0;
+            var ranked = true;
+
+            foreach (var mod in SelectedMods.Value)
+            {
+                multiplier *= mod.ScoreMultiplier;
+                ranked &= mod.Ranked;
+            }
+
+            MultiplierLabel.Text = $"{multiplier:N2}x";
+            if (multiplier > 1.0)
+                MultiplierLabel.FadeColour(HighMultiplierColour, 200);
+            else if (multiplier < 1.0)
+                MultiplierLabel.FadeColour(LowMultiplierColour, 200);
+            else
+                MultiplierLabel.FadeColour(Color4.White, 200);
+
+            UnrankedLabel.FadeTo(ranked ? 0 : 1, 200);
+        }
+
+        private void modButtonPressed(Mod selectedMod)
+        {
+            if (selectedMod != null)
+            {
+                if (State == Visibility.Visible) sampleOn?.Play();
+                DeselectTypes(selectedMod.IncompatibleMods, true);
+            }
+            else
+            {
+                if (State == Visibility.Visible) sampleOff?.Play();
+            }
+
+            refreshSelectedMods();
+        }
+
+        private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
+
+        #region Disposal
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+
+            Ruleset.UnbindAll();
+            SelectedMods.UnbindAll();
+        }
+
+        #endregion
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index 8639acfc94..b459afcb49 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -49,7 +49,6 @@ namespace osu.Game.Overlays.Profile.Sections
                 {
                     AutoSizeAxes = Axes.Y,
                     RelativeSizeAxes = Axes.X,
-                    Direction = FillDirection.Vertical,
                     Spacing = new Vector2(0, 2),
                 },
                 MoreButton = new ShowMoreButton
diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs
index b671d0e0fd..f063898a9f 100644
--- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs
@@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
                     LabelText = "Bypass caching (slow)",
                     Bindable = config.GetBindable<bool>(DebugSetting.BypassCaching)
                 },
+                new SettingsCheckbox
+                {
+                    LabelText = "Bypass front-to-back render pass",
+                    Bindable = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
+                }
             };
         }
     }
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 58d2eb1f1e..36c4fb5252 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
 
         private Bindable<ScalingMode> scalingMode;
         private Bindable<Size> sizeFullscreen;
-        private readonly BindableList<WindowMode> windowModes = new BindableList<WindowMode>();
+        private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
 
         private OsuGameBase game;
         private SettingsDropdown<Size> resolutionDropdown;
diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
index 84a41b6547..90412ec1d1 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Toolbar
         public ToolbarRulesetSelector()
         {
             RelativeSizeAxes = Axes.Y;
+            AutoSizeAxes = Axes.X;
 
             Children = new[]
             {
@@ -111,12 +112,6 @@ namespace osu.Game.Overlays.Toolbar
 
         private void disabledChanged(bool isDisabled) => this.FadeColour(isDisabled ? Color4.Gray : Color4.White, 300);
 
-        protected override void Update()
-        {
-            base.Update();
-            Size = new Vector2(modeButtons.DrawSize.X, 1);
-        }
-
         private void rulesetChanged(ValueChangedEvent<RulesetInfo> e)
         {
             foreach (ToolbarRulesetButton m in modeButtons.Children.Cast<ToolbarRulesetButton>())
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index e174a25df3..405d21c711 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Mods
                     shader.GetUniform<Vector2>("flashlightSize").UpdateValue(ref flashlightSize);
                     shader.GetUniform<float>("flashlightDim").UpdateValue(ref flashlightDim);
 
-                    Texture.WhitePixel.DrawQuad(screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction);
+                    DrawQuad(Texture.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction);
 
                     shader.Unbind();
                 }
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 7db24d36a5..52fba9cab3 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI
         /// <summary>
         /// The playfield.
         /// </summary>
-        public Playfield Playfield => playfield.Value;
+        public override Playfield Playfield => playfield.Value;
 
         /// <summary>
         /// Place to put drawables above hit objects but below UI.
@@ -342,6 +342,11 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         public readonly BindableBool IsPaused = new BindableBool();
 
+        /// <summary>
+        /// The playfield.
+        /// </summary>
+        public abstract Playfield Playfield { get; }
+
         /// <summary>
         /// The frame-stable clock which is being used for playfield display.
         /// </summary>
diff --git a/osu.Game/Scoring/ScoreRank.cs b/osu.Game/Scoring/ScoreRank.cs
index a93d015f1b..696d493830 100644
--- a/osu.Game/Scoring/ScoreRank.cs
+++ b/osu.Game/Scoring/ScoreRank.cs
@@ -7,10 +7,7 @@ namespace osu.Game.Scoring
 {
     public enum ScoreRank
     {
-        [Description(@"F")]
-        F,
-
-        [Description(@"F")]
+        [Description(@"D")]
         D,
 
         [Description(@"C")]
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index 2925689d20..c6de5857c2 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -189,7 +189,6 @@ namespace osu.Game.Screens.Menu
                 base.Draw(vertexAction);
 
                 shader.Bind();
-                texture.TextureGL.Bind();
 
                 Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy;
 
@@ -224,7 +223,8 @@ namespace osu.Game.Screens.Menu
                                 Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix)
                             );
 
-                            texture.DrawQuad(
+                            DrawQuad(
+                                texture,
                                 rectangle,
                                 colourInfo,
                                 null,
diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs
new file mode 100644
index 0000000000..a3caffb620
--- /dev/null
+++ b/osu.Game/Screens/Play/FailAnimation.cs
@@ -0,0 +1,113 @@
+// 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.Audio;
+using osu.Framework.Bindables;
+using osu.Game.Rulesets.UI;
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.MathUtils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play
+{
+    /// <summary>
+    /// Manage the animation to be applied when a player fails.
+    /// Single file; automatically disposed after use.
+    /// </summary>
+    public class FailAnimation : Component
+    {
+        public Action OnComplete;
+
+        private readonly DrawableRuleset drawableRuleset;
+
+        private readonly BindableDouble trackFreq = new BindableDouble(1);
+
+        private Track track;
+
+        private const float duration = 2500;
+
+        private SampleChannel failSample;
+
+        public FailAnimation(DrawableRuleset drawableRuleset)
+        {
+            this.drawableRuleset = drawableRuleset;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(AudioManager audio, IBindable<WorkingBeatmap> beatmap)
+        {
+            track = beatmap.Value.Track;
+            failSample = audio.Samples.Get(@"Gameplay/failsound");
+        }
+
+        private bool started;
+
+        /// <summary>
+        /// Start the fail animation playing.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">Thrown if started more than once.</exception>
+        public void Start()
+        {
+            if (started) throw new InvalidOperationException("Animation cannot be started more than once.");
+
+            started = true;
+
+            failSample.Play();
+
+            this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
+            {
+                OnComplete?.Invoke();
+                Expire();
+            });
+
+            track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
+
+            applyToPlayfield(drawableRuleset.Playfield);
+            drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500);
+            drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2);
+        }
+
+        protected override void Update()
+        {
+            base.Update();
+
+            if (!started)
+                return;
+
+            applyToPlayfield(drawableRuleset.Playfield);
+        }
+
+        private readonly List<DrawableHitObject> appliedObjects = new List<DrawableHitObject>();
+
+        private void applyToPlayfield(Playfield playfield)
+        {
+            foreach (var nested in playfield.NestedPlayfields)
+                applyToPlayfield(nested);
+
+            foreach (DrawableHitObject obj in playfield.HitObjectContainer.AliveObjects)
+            {
+                if (appliedObjects.Contains(obj))
+                    continue;
+
+                obj.RotateTo(RNG.NextSingle(-90, 90), duration);
+                obj.ScaleTo(obj.Scale * 0.5f, duration);
+                obj.MoveToOffset(new Vector2(0, 400), duration);
+                appliedObjects.Add(obj);
+            }
+        }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
+        }
+    }
+}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index c76339c848..22a0e65f91 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// 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;
@@ -176,7 +176,8 @@ namespace osu.Game.Screens.Play
                         fadeOut(true);
                         Restart();
                     },
-                }
+                },
+                failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }
             };
 
             DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true);
@@ -348,13 +349,13 @@ namespace osu.Game.Screens.Play
 
         protected FailOverlay FailOverlay { get; private set; }
 
+        private FailAnimation failAnimation;
+
         private bool onFail()
         {
             if (Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
                 return false;
 
-            GameplayClockContainer.Stop();
-
             HasFailed = true;
 
             // There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
@@ -363,9 +364,17 @@ namespace osu.Game.Screens.Play
             if (PauseOverlay.State == Visibility.Visible)
                 PauseOverlay.Hide();
 
+            failAnimation.Start();
+            return true;
+        }
+
+        // Called back when the transform finishes
+        private void onFailComplete()
+        {
+            GameplayClockContainer.Stop();
+
             FailOverlay.Retries = RestartCount;
             FailOverlay.Show();
-            return true;
         }
 
         #endregion
@@ -492,6 +501,13 @@ namespace osu.Game.Screens.Play
                 // still want to block if we are within the cooldown period and not already paused.
                 return true;
 
+            if (HasFailed && ValidForResume && !FailOverlay.IsPresent)
+                // ValidForResume is false when restarting
+            {
+                failAnimation.FinishTransforms(true);
+                return true;
+            }
+
             GameplayClockContainer.ResetLocalAdjustments();
 
             fadeOut();
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 5c334b126c..f1951e27ab 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Screens.Select.Carousel
                 case SortMode.Author:
                     return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase);
 
+                case SortMode.DateAdded:
+                    return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
+
                 case SortMode.Difficulty:
                     return BeatmapSet.MaxStarDifficulty.CompareTo(otherSet.BeatmapSet.MaxStarDifficulty);
             }
diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs
index 07318b473a..187ea910a7 100644
--- a/osu.Game/Skinning/SkinInfo.cs
+++ b/osu.Game/Skinning/SkinInfo.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using osu.Game.Configuration;
 using osu.Game.Database;
 
 namespace osu.Game.Skinning
@@ -19,6 +20,8 @@ namespace osu.Game.Skinning
 
         public List<SkinFileInfo> Files { get; set; }
 
+        public List<DatabasedSetting> Settings { get; set; }
+
         public bool DeletePending { get; set; }
 
         public string FullName => $"\"{Name}\" by {Creator}";
diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs
index 31cadb0a24..153eeda130 100644
--- a/osu.Game/Skinning/SkinStore.cs
+++ b/osu.Game/Skinning/SkinStore.cs
@@ -1,6 +1,8 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Linq;
+using Microsoft.EntityFrameworkCore;
 using osu.Framework.Platform;
 using osu.Game.Database;
 
@@ -12,5 +14,9 @@ namespace osu.Game.Skinning
             : base(contextFactory, storage)
         {
         }
+
+        protected override IQueryable<SkinInfo> AddIncludesForDeletion(IQueryable<SkinInfo> query) =>
+            base.AddIncludesForDeletion(query)
+                .Include(s => s.Settings); // don't include FileInfo. these are handled by the FileStore itself.
     }
 }
diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs
index a7a7f88ff7..86191609a4 100644
--- a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs
@@ -3,8 +3,13 @@
 
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
 using osu.Framework.Testing.Input;
 using osu.Game.Graphics.Cursor;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+using osuTK.Graphics;
 
 namespace osu.Game.Tests.Visual
 {
@@ -15,21 +20,92 @@ namespace osu.Game.Tests.Visual
 
         protected readonly ManualInputManager InputManager;
 
+        private readonly TriangleButton buttonTest;
+        private readonly TriangleButton buttonLocal;
+
         protected ManualInputManagerTestScene()
         {
-            base.Content.Add(InputManager = new ManualInputManager
+            base.Content.AddRange(new Drawable[]
             {
-                UseParentInput = true,
-                Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both },
+                InputManager = new ManualInputManager
+                {
+                    UseParentInput = true,
+                    Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both },
+                },
+                new Container
+                {
+                    AutoSizeAxes = Axes.Both,
+                    Anchor = Anchor.TopRight,
+                    Origin = Anchor.TopRight,
+                    Margin = new MarginPadding(5),
+                    CornerRadius = 5,
+                    Masking = true,
+                    Children = new Drawable[]
+                    {
+                        new Box
+                        {
+                            Colour = Color4.Black,
+                            RelativeSizeAxes = Axes.Both,
+                            Alpha = 0.5f,
+                        },
+                        new FillFlowContainer
+                        {
+                            AutoSizeAxes = Axes.Both,
+                            Direction = FillDirection.Vertical,
+                            Margin = new MarginPadding(5),
+                            Spacing = new Vector2(5),
+                            Children = new Drawable[]
+                            {
+                                new OsuSpriteText
+                                {
+                                    Anchor = Anchor.TopCentre,
+                                    Origin = Anchor.TopCentre,
+                                    Text = "Input Priority"
+                                },
+                                new FillFlowContainer
+                                {
+                                    AutoSizeAxes = Axes.Both,
+                                    Anchor = Anchor.TopCentre,
+                                    Origin = Anchor.TopCentre,
+                                    Margin = new MarginPadding(5),
+                                    Spacing = new Vector2(5),
+                                    Direction = FillDirection.Horizontal,
+
+                                    Children = new Drawable[]
+                                    {
+                                        buttonLocal = new TriangleButton
+                                        {
+                                            Text = "local",
+                                            Size = new Vector2(50, 30),
+                                            Action = returnUserInput
+                                        },
+                                        buttonTest = new TriangleButton
+                                        {
+                                            Text = "test",
+                                            Size = new Vector2(50, 30),
+                                            Action = returnTestInput
+                                        },
+                                    }
+                                },
+                            }
+                        },
+                    }
+                },
             });
         }
 
-        /// <summary>
-        /// Returns input back to the user.
-        /// </summary>
-        protected void ReturnUserInput()
+        protected override void Update()
         {
-            AddStep("Return user input", () => InputManager.UseParentInput = true);
+            base.Update();
+
+            buttonTest.Enabled.Value = InputManager.UseParentInput;
+            buttonLocal.Enabled.Value = !InputManager.UseParentInput;
         }
+
+        private void returnUserInput() =>
+            InputManager.UseParentInput = true;
+
+        private void returnTestInput() =>
+            InputManager.UseParentInput = false;
     }
 }
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f84bb64fbf..eeb1f2bee3 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -14,8 +14,8 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2019.518.0" />
-    <PackageReference Include="ppy.osu.Framework" Version="2019.604.1" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2019.609.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2019.607.0" />
     <PackageReference Include="SharpCompress" Version="0.23.0" />
     <PackageReference Include="NUnit" Version="3.12.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />
diff --git a/osu.iOS.props b/osu.iOS.props
index fc047aa5f0..3a5090d968 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -104,9 +104,9 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
-    <PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" />
-    <PackageReference Include="ppy.osu.Framework" Version="2019.523.0" />
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.523.0" />
+    <PackageReference Include="ppy.osu.Game.Resources" Version="2019.518.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2019.607.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.607.0" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />