diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000000..0c6b80e97e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: https://osu.ppy.sh/home/support
diff --git a/README.md b/README.md
index abddb1faa1..91ea34e999 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 reulgarly 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.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index 9cec0d280d..ab3c040b4e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -20,14 +20,14 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
             var beatmap = new Beatmap
             {
                 BeatmapInfo = new BeatmapInfo
                 {
                     BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
-                    Ruleset = ruleset.RulesetInfo
+                    Ruleset = ruleset
                 }
             };
 
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
index 035bbe4b4e..0ad72412fc 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs
@@ -29,14 +29,14 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
             var beatmap = new Beatmap
             {
                 BeatmapInfo = new BeatmapInfo
                 {
                     BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
-                    Ruleset = ruleset.RulesetInfo
+                    Ruleset = ruleset
                 }
             };
 
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
index 7d7528372a..9ce46ad6ba 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
@@ -16,14 +16,14 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
             var beatmap = new Beatmap
             {
                 BeatmapInfo = new BeatmapInfo
                 {
                     BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
-                    Ruleset = ruleset.RulesetInfo
+                    Ruleset = ruleset
                 }
             };
 
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index 7393f75e5a..9cbff8c5d3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -23,13 +23,13 @@ namespace osu.Game.Rulesets.Catch.Tests
             AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
         }
 
-        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
             var beatmap = new Beatmap
             {
                 BeatmapInfo =
                 {
-                    Ruleset = ruleset.RulesetInfo,
+                    Ruleset = ruleset,
                     BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f }
                 }
             };
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
index 921246751c..399cf22599 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs
@@ -17,14 +17,14 @@ namespace osu.Game.Rulesets.Osu.Tests
         {
         }
 
-        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
         {
             var beatmap = new Beatmap
             {
                 BeatmapInfo = new BeatmapInfo
                 {
                     BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 },
-                    Ruleset = ruleset.RulesetInfo
+                    Ruleset = ruleset
                 }
             };
 
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 193cfe9c94..2eb783233a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.Replays;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring;
 using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
 using osu.Game.Tests.Visual;
 using osuTK;
 
@@ -299,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.Tests
         {
             AddStep("load player", () =>
             {
-                Beatmap.Value = new TestWorkingBeatmap(new Beatmap<OsuHitObject>
+                Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
                 {
                     HitObjects =
                     {
@@ -323,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Tests
                         BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
                         Ruleset = new OsuRuleset().RulesetInfo
                     },
-                }, Clock);
+                });
 
                 var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
 
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.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
index 3634ec7d4a..6f9856df83 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
@@ -18,7 +18,6 @@ using osu.Game.Rulesets.Taiko.Judgements;
 using osu.Game.Rulesets.Taiko.Objects;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.UI;
-using osu.Game.Tests.Beatmaps;
 using osu.Game.Tests.Visual;
 using osuTK;
 using osu.Game.Rulesets.Scoring;
@@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
             var controlPointInfo = new ControlPointInfo();
             controlPointInfo.TimingPoints.Add(new TimingControlPoint());
 
-            WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap
+            WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
             {
                 HitObjects = new List<HitObject> { new CentreHit() },
                 BeatmapInfo = new BeatmapInfo
@@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
                     Ruleset = new TaikoRuleset().RulesetInfo
                 },
                 ControlPointInfo = controlPointInfo
-            }, Clock);
+            });
 
             Add(playfieldContainer = new Container
             {
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
index c9bdcf928f..7104a420a3 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenBeatmap.cs
@@ -7,6 +7,7 @@ using System.Linq;
 using System.Threading;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Audio;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Events;
@@ -54,7 +55,7 @@ namespace osu.Game.Tests.Visual.Background
         private RulesetStore rulesets;
 
         [BackgroundDependencyLoader]
-        private void load(GameHost host)
+        private void load(GameHost host, AudioManager audio)
         {
             factory = new DatabaseContextFactory(LocalStorage);
             factory.ResetDatabase();
@@ -68,7 +69,7 @@ namespace osu.Game.Tests.Visual.Background
                 usage.Migrate();
 
             Dependencies.Cache(rulesets = new RulesetStore(factory));
-            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, Beatmap.Default));
+            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, Beatmap.Default));
             Dependencies.Cache(new OsuConfigManager(LocalStorage));
 
             manager.Import(TestResources.GetTestBeatmapForImport());
diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
index 94412455a0..df6740421b 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
@@ -111,16 +111,19 @@ namespace osu.Game.Tests.Visual.Components
 
         private class TestPreviewTrackManager : PreviewTrackManager
         {
-            protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TestPreviewTrack(beatmapSetInfo, trackManager);
+            protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
 
             protected class TestPreviewTrack : TrackManagerPreviewTrack
             {
-                public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager)
+                private readonly ITrackStore trackManager;
+
+                public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
                     : base(beatmapSetInfo, trackManager)
                 {
+                    this.trackManager = trackManager;
                 }
 
-                protected override Track GetTrack() => new TrackVirtual { Length = 100000 };
+                protected override Track GetTrack() => trackManager.GetVirtual(100000);
             }
         }
     }
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs
index b537cb0beb..608df1965e 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorCompose.cs
@@ -7,7 +7,6 @@ using NUnit.Framework;
 using osu.Framework.Allocation;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Edit.Compose;
-using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual.Editor
 {
@@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.Editor
         [BackgroundDependencyLoader]
         private void load()
         {
-            Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, Clock);
+            Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
             Child = new ComposeScreen();
         }
     }
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index 154c58dd99..a8c2362910 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Audio;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
@@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Editor
         };
 
         [BackgroundDependencyLoader]
-        private void load()
+        private void load(AudioManager audio)
         {
-            Beatmap.Value = new WaveformTestBeatmap();
+            Beatmap.Value = new WaveformTestBeatmap(audio);
 
             Children = new Drawable[]
             {
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
index 590fa59107..b997d6aaeb 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.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 NUnit.Framework;
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.ControlPoints;
 using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 using osuTK.Graphics;
 
@@ -48,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editor
                 }
             };
 
-            Beatmap.Value = new TestWorkingBeatmap(testBeatmap, Clock);
+            Beatmap.Value = CreateWorkingBeatmap(testBeatmap);
 
             Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock };
         }
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
index f20c921ff2..2e04eb50ca 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Edit.Components.Timelines.Summary;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 
 namespace osu.Game.Tests.Visual.Editor
@@ -21,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editor
         [BackgroundDependencyLoader]
         private void load()
         {
-            Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
+            Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
 
             Add(new SummaryTimeline
             {
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
index 47aa059b62..7accbe2fa8 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
@@ -18,7 +18,6 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Screens.Edit.Compose;
 using osu.Game.Screens.Edit.Compose.Components;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 
 namespace osu.Game.Tests.Visual.Editor
@@ -45,7 +44,7 @@ namespace osu.Game.Tests.Visual.Editor
         [BackgroundDependencyLoader]
         private void load()
         {
-            Beatmap.Value = new TestWorkingBeatmap(new Beatmap
+            Beatmap.Value = CreateWorkingBeatmap(new Beatmap
             {
                 HitObjects = new List<HitObject>
                 {
diff --git a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
index 126ab98291..0d4fe4366d 100644
--- a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.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 NUnit.Framework;
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Screens.Edit.Components;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 
 namespace osu.Game.Tests.Visual.Editor
@@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Editor
                 Size = new Vector2(200, 100)
             };
 
-            Beatmap.Value = new TestWorkingBeatmap(new Beatmap(), Clock);
+            Beatmap.Value = CreateWorkingBeatmap(new Beatmap());
 
             Child = playback;
         }
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
index e93789b1d3..e2762f3d5f 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs
@@ -3,6 +3,7 @@
 
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Audio;
@@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Osu;
 using osuTK.Graphics;
 
 namespace osu.Game.Tests.Visual.Editor
@@ -20,9 +22,9 @@ namespace osu.Game.Tests.Visual.Editor
         private WorkingBeatmap waveformBeatmap;
 
         [BackgroundDependencyLoader]
-        private void load()
+        private void load(AudioManager audio)
         {
-            waveformBeatmap = new WaveformTestBeatmap();
+            waveformBeatmap = new WaveformTestBeatmap(audio);
         }
 
         [TestCase(1f)]
@@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Editor
                     Child = graph = new TestWaveformGraph
                     {
                         RelativeSizeAxes = Axes.Both,
-                        Waveform = new DummyWorkingBeatmap().Waveform,
+                        Waveform = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Waveform,
                     },
                 };
             });
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 5c26f733ab..daee3a520c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.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;
@@ -16,7 +16,6 @@ using osu.Game.Rulesets.Scoring;
 using osu.Game.Scoring;
 using osu.Game.Screens;
 using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual.Gameplay
 {
@@ -29,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         public void Setup() => Schedule(() =>
         {
             InputManager.Child = stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both };
-            Beatmap.Value = new TestWorkingBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), Clock);
+            Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
         });
 
         [Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
index c75fb2567b..65b56319e8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerReferenceLeaking.cs
@@ -3,7 +3,6 @@
 
 using System;
 using osu.Framework.Lists;
-using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
 using osu.Game.Screens.Play;
@@ -43,9 +42,9 @@ namespace osu.Game.Tests.Visual.Gameplay
             });
         }
 
-        protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock clock)
+        protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap)
         {
-            var working = base.CreateWorkingBeatmap(beatmap, clock);
+            var working = base.CreateWorkingBeatmap(beatmap);
             workingWeakReferences.Add(working);
             return working;
         }
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
index 21b97fe73b..8091e93471 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.Multiplayer;
@@ -57,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
             AddStep("set name", () => Room.Name.Value = "Room name");
             AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
 
-            AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = new DummyWorkingBeatmap().BeatmapInfo }));
+            AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo }));
             AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
 
             AddStep("clear name", () => Room.Name.Value = "");
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
index a3d932a383..8b67892fbb 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Overlays.Direct;
 using osu.Game.Rulesets.Osu;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 
 namespace osu.Game.Tests.Visual.Online
@@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online
         [BackgroundDependencyLoader]
         private void load()
         {
-            var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
+            var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
             beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true;
             beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true;
 
diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
new file mode 100644
index 0000000000..bccb263600
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Profile.Sections;
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Tests.Visual.Online
+{
+    public class TestSceneShowMoreButton : OsuTestScene
+    {
+        public override IReadOnlyList<Type> RequiredTypes => new[]
+        {
+            typeof(ShowMoreButton),
+        };
+
+        public TestSceneShowMoreButton()
+        {
+            ShowMoreButton button = null;
+
+            int fireCount = 0;
+
+            Add(button = new ShowMoreButton
+            {
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                Action = () =>
+                {
+                    fireCount++;
+                    // ReSharper disable once AccessToModifiedClosure
+                    // ReSharper disable once PossibleNullReferenceException
+                    Scheduler.AddDelayed(() => button.IsLoading = false, 2000);
+                }
+            });
+
+            AddStep("click button", () => button.Click());
+
+            AddAssert("action fired once", () => fireCount == 1);
+            AddAssert("is in loading state", () => button.IsLoading);
+
+            AddStep("click button", () => button.Click());
+
+            AddAssert("action not fired", () => fireCount == 1);
+            AddAssert("is in loading state", () => button.IsLoading);
+
+            AddUntilStep("wait for loaded", () => !button.IsLoading);
+
+            AddStep("click button", () => button.Click());
+
+            AddAssert("action fired twice", () => fireCount == 2);
+            AddAssert("is in loading state", () => button.IsLoading);
+        }
+    }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs
index cf4362ba28..8395ece457 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using NUnit.Framework;
+using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Game.Beatmaps;
 using osu.Game.Screens.Select;
@@ -18,7 +19,8 @@ namespace osu.Game.Tests.Visual.SongSelect
     {
         public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(BeatmapDetails) };
 
-        public TestSceneBeatmapDetailArea()
+        [BackgroundDependencyLoader]
+        private void load(OsuGameBase game)
         {
             BeatmapDetailArea detailsArea;
             Add(detailsArea = new BeatmapDetailArea
@@ -28,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 Size = new Vector2(550f, 450f),
             });
 
-            AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+            AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
                 {
                     BeatmapInfo =
                     {
@@ -56,7 +58,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 }
             );
 
-            AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+            AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
             {
                 BeatmapInfo =
                 {
@@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 }
             });
 
-            AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+            AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
             {
                 BeatmapInfo =
                 {
@@ -107,7 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 }
             });
 
-            AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+            AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
             {
                 BeatmapInfo =
                 {
@@ -133,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 }
             });
 
-            AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+            AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
             {
                 BeatmapInfo =
                 {
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
index b1ed5c46c2..9969795ecf 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.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.Collections.Generic;
@@ -18,7 +18,6 @@ using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Osu;
 using osu.Game.Rulesets.Taiko;
 using osu.Game.Screens.Select;
-using osu.Game.Tests.Beatmaps;
 using osuTK;
 
 namespace osu.Game.Tests.Visual.SongSelect
@@ -136,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelect
             AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
             {
                 infoBefore = infoWedge.Info;
-                infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b);
+                infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
             });
 
             AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore);
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 7e962dbc06..ebee358730 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -8,6 +8,7 @@ using System.Linq;
 using System.Text;
 using NUnit.Framework;
 using osu.Framework.Allocation;
+using osu.Framework.Audio;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions;
 using osu.Framework.MathUtils;
@@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.SongSelect
         }
 
         [BackgroundDependencyLoader]
-        private void load(GameHost host)
+        private void load(GameHost host, AudioManager audio)
         {
             factory = new DatabaseContextFactory(LocalStorage);
             factory.ResetDatabase();
@@ -93,7 +94,7 @@ namespace osu.Game.Tests.Visual.SongSelect
                 usage.Migrate();
 
             Dependencies.Cache(rulesets = new RulesetStore(factory));
-            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, defaultBeatmap = Beatmap.Default));
+            Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default));
 
             Beatmap.SetDefault();
         }
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/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.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
index 23065629a6..f59458ef8d 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.UserInterface
             TestUpdateableBeatmapBackgroundSprite background = null;
 
             AddStep("load null beatmap", () => Child = background = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both });
-            AddUntilStep("wait for load", () => background.ContentLoaded);
+            AddUntilStep("content loaded", () => background.ContentLoaded);
         }
 
         [Test]
diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index f66b374cd7..fdb91b7c5b 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.cs
@@ -3,6 +3,7 @@
 
 using System.IO;
 using System.Linq;
+using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Graphics.Textures;
 using osu.Game.Beatmaps;
@@ -19,12 +20,14 @@ namespace osu.Game.Tests
     {
         private readonly ZipArchiveReader reader;
         private readonly Stream stream;
+        private readonly ITrackStore trackStore;
 
-        public WaveformTestBeatmap()
-            : base(new BeatmapInfo())
+        public WaveformTestBeatmap(AudioManager audioManager)
+            : base(new BeatmapInfo(), audioManager)
         {
             stream = TestResources.GetTestBeatmapStream();
             reader = new ZipArchiveReader(stream);
+            trackStore = audioManager.GetTrackStore(reader);
         }
 
         public override void Dispose()
@@ -32,17 +35,19 @@ namespace osu.Game.Tests
             base.Dispose();
             stream?.Dispose();
             reader?.Dispose();
+            trackStore?.Dispose();
         }
 
         protected override IBeatmap GetBeatmap() => createTestBeatmap();
 
         protected override Texture GetBackground() => null;
 
-        protected override Waveform GetWaveform() => new Waveform(getAudioStream());
+        protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
 
-        protected override Track GetTrack() => new TrackBass(getAudioStream());
+        protected override Track GetTrack() => trackStore.Get(firstAudioFile);
+
+        private string firstAudioFile => reader.Filenames.First(f => f.EndsWith(".mp3"));
 
-        private Stream getAudioStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".mp3")));
         private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")));
 
         private Beatmap createTestBeatmap()
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index 99c0d70ac9..d479483508 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -20,19 +20,18 @@ namespace osu.Game.Audio
         private readonly BindableDouble muteBindable = new BindableDouble();
 
         private AudioManager audio;
-        private TrackManager trackManager;
+        private ITrackStore trackStore;
 
         private TrackManagerPreviewTrack current;
 
         [BackgroundDependencyLoader]
         private void load(AudioManager audio, FrameworkConfigManager config)
         {
-            trackManager = new TrackManager(new OnlineStore());
+            trackStore = audio.GetTrackStore(new OnlineStore());
 
             this.audio = audio;
-            audio.AddItem(trackManager);
 
-            config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume);
+            config.BindWith(FrameworkSetting.VolumeMusic, trackStore.Volume);
         }
 
         /// <summary>
@@ -42,19 +41,19 @@ namespace osu.Game.Audio
         /// <returns>The playable <see cref="PreviewTrack"/>.</returns>
         public PreviewTrack Get(BeatmapSetInfo beatmapSetInfo)
         {
-            var track = CreatePreviewTrack(beatmapSetInfo, trackManager);
+            var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
 
             track.Started += () =>
             {
                 current?.Stop();
                 current = track;
-                audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable);
+                audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
             };
 
             track.Stopped += () =>
             {
                 current = null;
-                audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
+                audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
             };
 
             return track;
@@ -81,16 +80,16 @@ namespace osu.Game.Audio
         /// <summary>
         /// Creates the <see cref="TrackManagerPreviewTrack"/>.
         /// </summary>
-        protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager) => new TrackManagerPreviewTrack(beatmapSetInfo, trackManager);
+        protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore);
 
         protected class TrackManagerPreviewTrack : PreviewTrack
         {
             public IPreviewTrackOwner Owner { get; private set; }
 
             private readonly BeatmapSetInfo beatmapSetInfo;
-            private readonly TrackManager trackManager;
+            private readonly ITrackStore trackManager;
 
-            public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, TrackManager trackManager)
+            public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
             {
                 this.beatmapSetInfo = beatmapSetInfo;
                 this.trackManager = trackManager;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 798bca3ada..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
             };
         }
 
@@ -429,7 +430,7 @@ namespace osu.Game.Beatmaps
             private readonly IBeatmap beatmap;
 
             public DummyConversionBeatmap(IBeatmap beatmap)
-                : base(beatmap.BeatmapInfo)
+                : base(beatmap.BeatmapInfo, null)
             {
                 this.beatmap = beatmap;
             }
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 0bdab22dd2..4b1bddbf0d 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -20,14 +20,12 @@ namespace osu.Game.Beatmaps
         protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
         {
             private readonly IResourceStore<byte[]> store;
-            private readonly AudioManager audioManager;
 
             public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager)
-                : base(beatmapInfo)
+                : base(beatmapInfo, audioManager)
             {
                 this.store = store;
                 this.textureStore = textureStore;
-                this.audioManager = audioManager;
             }
 
             protected override IBeatmap GetBeatmap()
@@ -47,6 +45,8 @@ namespace osu.Game.Beatmaps
 
             private TextureStore textureStore;
 
+            private ITrackStore trackStore;
+
             protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
 
             protected override Texture GetBackground()
@@ -68,8 +68,7 @@ namespace osu.Game.Beatmaps
             {
                 try
                 {
-                    var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
-                    return trackData == null ? null : new TrackBass(trackData);
+                    return (trackStore ?? (trackStore = AudioManager.GetTrackStore(store))).Get(getPathForFile(Metadata.AudioFile));
                 }
                 catch
                 {
@@ -77,6 +76,14 @@ namespace osu.Game.Beatmaps
                 }
             }
 
+            public override void RecycleTrack()
+            {
+                base.RecycleTrack();
+
+                trackStore?.Dispose();
+                trackStore = null;
+            }
+
             public override void TransferTo(WorkingBeatmap other)
             {
                 base.TransferTo(other);
@@ -135,7 +142,7 @@ namespace osu.Game.Beatmaps
 
                 try
                 {
-                    skin = new LegacyBeatmapSkin(BeatmapInfo, store, audioManager);
+                    skin = new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager);
                 }
                 catch (Exception e)
                 {
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/Beatmaps/BindableBeatmap.cs b/osu.Game/Beatmaps/BindableBeatmap.cs
index 27bad65062..af627cc6a9 100644
--- a/osu.Game/Beatmaps/BindableBeatmap.cs
+++ b/osu.Game/Beatmaps/BindableBeatmap.cs
@@ -1,11 +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.Diagnostics;
-using JetBrains.Annotations;
-using osu.Framework.Audio;
-using osu.Framework.Audio.Track;
 using osu.Framework.Bindables;
 
 namespace osu.Game.Beatmaps
@@ -16,32 +12,15 @@ namespace osu.Game.Beatmaps
     /// </summary>
     public abstract class BindableBeatmap : NonNullableBindable<WorkingBeatmap>
     {
-        private AudioManager audioManager;
         private WorkingBeatmap lastBeatmap;
 
         protected BindableBeatmap(WorkingBeatmap defaultValue)
             : base(defaultValue)
         {
+            BindValueChanged(b => updateAudioTrack(b.NewValue), true);
         }
 
-        /// <summary>
-        /// Registers an <see cref="AudioManager"/> for <see cref="Track"/>s to be added to.
-        /// </summary>
-        /// <param name="audioManager">The <see cref="AudioManager"/> to register.</param>
-        protected void RegisterAudioManager([NotNull] AudioManager audioManager)
-        {
-            if (this.audioManager != null) throw new InvalidOperationException($"Cannot register multiple {nameof(AudioManager)}s.");
-
-            this.audioManager = audioManager;
-
-            ValueChanged += b => registerAudioTrack(b.NewValue);
-
-            // If the track has changed prior to this being called, let's register it
-            if (Value != Default)
-                registerAudioTrack(Value);
-        }
-
-        private void registerAudioTrack(WorkingBeatmap beatmap)
+        private void updateAudioTrack(WorkingBeatmap beatmap)
         {
             var trackLoaded = lastBeatmap?.TrackLoaded ?? false;
 
@@ -55,18 +34,9 @@ namespace osu.Game.Beatmaps
 
                     lastBeatmap.RecycleTrack();
                 }
-
-                audioManager.Track.AddItem(beatmap.Track);
             }
 
             lastBeatmap = beatmap;
         }
-
-        /// <summary>
-        /// Retrieve a new <see cref="BindableBeatmap"/> instance weakly bound to this <see cref="BindableBeatmap"/>.
-        /// If you are further binding to events of the retrieved <see cref="BindableBeatmap"/>, ensure a local reference is held.
-        /// </summary>
-        [NotNull]
-        public new abstract BindableBeatmap GetBoundCopy();
     }
 }
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
index 96786f5f49..1fd3502799 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -1,6 +1,7 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -31,24 +32,8 @@ namespace osu.Game.Beatmaps.Drawables
         /// </summary>
         protected virtual double UnloadDelay => 10000;
 
-        private BeatmapInfo lastModel;
-        private bool firstLoad = true;
-
-        protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad)
-        {
-            return new DelayedLoadUnloadWrapper(() =>
-            {
-                // If DelayedLoadUnloadWrapper is attempting to RELOAD the same content (Beatmap), that means that it was
-                // previously UNLOADED and thus its children have been disposed of, so we need to recreate them here.
-                if (!firstLoad && lastModel == Beatmap.Value)
-                    return CreateDrawable(Beatmap.Value);
-
-                // If the model has changed since the previous unload (or if there was no load), then we can safely use the given content
-                lastModel = Beatmap.Value;
-                firstLoad = false;
-                return content;
-            }, timeBeforeLoad, UnloadDelay);
-        }
+        protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
+            => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
 
         protected override Drawable CreateDrawable(BeatmapInfo model)
         {
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 7d25ca3ede..3a4c677bd1 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using osu.Framework.Audio;
 using osu.Framework.Audio.Track;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics.Textures;
@@ -16,9 +17,9 @@ namespace osu.Game.Beatmaps
 {
     public class DummyWorkingBeatmap : WorkingBeatmap
     {
-        private readonly OsuGameBase game;
+        private readonly TextureStore textures;
 
-        public DummyWorkingBeatmap(OsuGameBase game = null)
+        public DummyWorkingBeatmap(AudioManager audio, TextureStore textures)
             : base(new BeatmapInfo
             {
                 Metadata = new BeatmapMetadata
@@ -34,16 +35,16 @@ namespace osu.Game.Beatmaps
                     OverallDifficulty = 0,
                 },
                 Ruleset = new DummyRulesetInfo()
-            })
+            }, audio)
         {
-            this.game = game;
+            this.textures = textures;
         }
 
         protected override IBeatmap GetBeatmap() => new Beatmap();
 
-        protected override Texture GetBackground() => game?.Textures.Get(@"Backgrounds/bg4");
+        protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
 
-        protected override Track GetTrack() => new TrackVirtual { Length = 1000 };
+        protected override Track GetTrack() => GetVirtualTrack();
 
         private class DummyRulesetInfo : RulesetInfo
         {
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 4b0720d867..328763fc9f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -11,15 +11,17 @@ using osu.Framework.IO.File;
 using System.IO;
 using System.Linq;
 using System.Threading;
+using osu.Framework.Audio;
 using osu.Game.IO.Serialization;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.UI;
 using osu.Game.Skinning;
 
 namespace osu.Game.Beatmaps
 {
-    public abstract partial class WorkingBeatmap : IDisposable
+    public abstract class WorkingBeatmap : IDisposable
     {
         public readonly BeatmapInfo BeatmapInfo;
 
@@ -27,8 +29,11 @@ namespace osu.Game.Beatmaps
 
         public readonly BeatmapMetadata Metadata;
 
-        protected WorkingBeatmap(BeatmapInfo beatmapInfo)
+        protected AudioManager AudioManager { get; }
+
+        protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
         {
+            AudioManager = audioManager;
             BeatmapInfo = beatmapInfo;
             BeatmapSetInfo = beatmapInfo.BeatmapSet;
             Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
@@ -46,13 +51,39 @@ namespace osu.Game.Beatmaps
                 return b;
             });
 
-            track = new RecyclableLazy<Track>(() => GetTrack() ?? new VirtualBeatmapTrack(Beatmap));
+            track = new RecyclableLazy<Track>(() => GetTrack() ?? GetVirtualTrack());
             background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid);
             waveform = new RecyclableLazy<Waveform>(GetWaveform);
             storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
             skin = new RecyclableLazy<Skin>(GetSkin);
         }
 
+        protected virtual Track GetVirtualTrack()
+        {
+            const double excess_length = 1000;
+
+            var lastObject = Beatmap.HitObjects.LastOrDefault();
+
+            double length;
+
+            switch (lastObject)
+            {
+                case null:
+                    length = excess_length;
+                    break;
+
+                case IHasEndTime endTime:
+                    length = endTime.EndTime + excess_length;
+                    break;
+
+                default:
+                    length = lastObject.StartTime + excess_length;
+                    break;
+            }
+
+            return AudioManager.Tracks.GetVirtual(length);
+        }
+
         /// <summary>
         /// Saves the <see cref="Beatmaps.Beatmap"/>.
         /// </summary>
@@ -150,6 +181,7 @@ namespace osu.Game.Beatmaps
 
         public bool SkinLoaded => skin.IsResultAvailable;
         public Skin Skin => skin.Value;
+
         protected virtual Skin GetSkin() => new DefaultSkin();
         private readonly RecyclableLazy<Skin> skin;
 
@@ -175,7 +207,7 @@ namespace osu.Game.Beatmaps
         /// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any).
         /// Accessing track again will load a fresh instance.
         /// </summary>
-        public void RecycleTrack() => track.Recycle();
+        public virtual void RecycleTrack() => track.Recycle();
 
         public class RecyclableLazy<T>
         {
diff --git a/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs b/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs
deleted file mode 100644
index 1e237a2b53..0000000000
--- a/osu.Game/Beatmaps/WorkingBeatmap_VirtualBeatmapTrack.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using osu.Framework.Audio.Track;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Types;
-
-namespace osu.Game.Beatmaps
-{
-    public partial class WorkingBeatmap
-    {
-        /// <summary>
-        /// A type of <see cref="TrackVirtual"/> which provides a valid length based on the <see cref="HitObject"/>s of an <see cref="IBeatmap"/>.
-        /// </summary>
-        protected class VirtualBeatmapTrack : TrackVirtual
-        {
-            private const double excess_length = 1000;
-
-            public VirtualBeatmapTrack(IBeatmap beatmap)
-            {
-                var lastObject = beatmap.HitObjects.LastOrDefault();
-
-                switch (lastObject)
-                {
-                    case null:
-                        Length = excess_length;
-                        break;
-
-                    case IHasEndTime endTime:
-                        Length = endTime.EndTime + excess_length;
-                        break;
-
-                    default:
-                        Length = lastObject.StartTime + excess_length;
-                        break;
-                }
-            }
-        }
-    }
-}
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/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 3f84f77081..8b34459710 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -51,8 +51,8 @@ namespace osu.Game.Graphics.Containers
             if (osuGame != null)
                 OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
 
-            samplePopIn = audio.Sample.Get(@"UI/overlay-pop-in");
-            samplePopOut = audio.Sample.Get(@"UI/overlay-pop-out");
+            samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
+            samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
 
             StateChanged += onStateChanged;
         }
diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
index c4f85926ee..67af79c763 100644
--- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs
@@ -24,7 +24,13 @@ namespace osu.Game.Graphics.Containers
         {
             Enabled.ValueChanged += e =>
             {
-                if (!e.NewValue) unhover();
+                if (isHovered)
+                {
+                    if (e.NewValue)
+                        fadeIn();
+                    else
+                        fadeOut();
+                }
             };
         }
 
@@ -32,27 +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;
+            if (!isHovered)
+                return;
 
             isHovered = false;
-            EffectTargets.ForEach(d => d.FadeColour(IdleColour, FADE_DURATION, Easing.OutQuint));
+            fadeOut();
+
+            base.OnHoverLost(e);
         }
 
         [BackgroundDependencyLoader]
@@ -67,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/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 24a98e6dc9..5ad5e5569a 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Graphics
             screenshotFormat = config.GetBindable<ScreenshotFormat>(OsuSetting.ScreenshotFormat);
             captureMenuCursor = config.GetBindable<bool>(OsuSetting.ScreenshotCaptureMenuCursor);
 
-            shutter = audio.Sample.Get("UI/shutter");
+            shutter = audio.Samples.Get("UI/shutter");
         }
 
         public bool OnPressed(GlobalAction action)
diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
index cbbaa6d303..70d988f60e 100644
--- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
-            sampleClick = audio.Sample.Get($@"UI/generic-select{SampleSet.GetDescription()}");
+            sampleClick = audio.Samples.Get($@"UI/generic-select{SampleSet.GetDescription()}");
         }
     }
 }
diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs
index b246092a7f..f1ac8ced6e 100644
--- a/osu.Game/Graphics/UserInterface/HoverSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
-            sampleHover = audio.Sample.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
+            sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
         }
     }
 
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/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 7a27f825f6..494d4e4262 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -17,11 +17,11 @@ namespace osu.Game.Graphics.UserInterface
     /// <summary>
     /// A button with added default sound effects.
     /// </summary>
-    public class OsuButton : Button
+    public abstract class OsuButton : Button
     {
         private Box hover;
 
-        public OsuButton()
+        protected OsuButton()
         {
             Height = 40;
 
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index 2944fc87af..cd1147e3d3 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -112,8 +112,8 @@ namespace osu.Game.Graphics.UserInterface
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
-            sampleChecked = audio.Sample.Get(@"UI/check-on");
-            sampleUnchecked = audio.Sample.Get(@"UI/check-off");
+            sampleChecked = audio.Samples.Get(@"UI/check-on");
+            sampleUnchecked = audio.Samples.Get(@"UI/check-off");
         }
     }
 }
diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs
index 32994be78a..f8234cb81f 100644
--- a/osu.Game/Graphics/UserInterface/OsuMenu.cs
+++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs
@@ -71,8 +71,8 @@ namespace osu.Game.Graphics.UserInterface
             [BackgroundDependencyLoader]
             private void load(AudioManager audio)
             {
-                sampleHover = audio.Sample.Get(@"UI/generic-hover");
-                sampleClick = audio.Sample.Get(@"UI/generic-select");
+                sampleHover = audio.Samples.Get(@"UI/generic-hover");
+                sampleClick = audio.Samples.Get(@"UI/generic-select");
 
                 BackgroundColour = Color4.Transparent;
                 BackgroundColourHover = OsuColour.FromHex(@"172023");
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index c3c447ef83..5c706781e6 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Graphics.UserInterface
         [BackgroundDependencyLoader]
         private void load(AudioManager audio, OsuColour colours)
         {
-            sample = audio.Sample.Get(@"UI/sliderbar-notch");
+            sample = audio.Samples.Get(@"UI/sliderbar-notch");
             AccentColour = colours.Pink;
         }
 
diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs
index a561523799..4ee7a19ebc 100644
--- a/osu.Game/IO/Archives/ArchiveReader.cs
+++ b/osu.Game/IO/Archives/ArchiveReader.cs
@@ -15,6 +15,8 @@ namespace osu.Game.IO.Archives
         /// </summary>
         public abstract Stream GetStream(string name);
 
+        public IEnumerable<string> GetAvailableResources() => Filenames;
+
         public abstract void Dispose();
 
         /// <summary>
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/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 2efc9f4968..3af11ff20f 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -81,6 +81,9 @@ namespace osu.Game.Online.Chat
             if (user == null)
                 throw new ArgumentNullException(nameof(user));
 
+            if (user.Id == api.LocalUser.Value.Id)
+                return;
+
             CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id))
                                    ?? new Channel(user);
         }
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7b9aed8364..f9128687d6 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -161,7 +161,7 @@ namespace osu.Game
 
             dependencies.CacheAs<IAPIProvider>(API);
 
-            var defaultBeatmap = new DummyWorkingBeatmap(this);
+            var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
 
             dependencies.Cache(RulesetStore = new RulesetStore(contextFactory));
             dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
@@ -193,9 +193,9 @@ namespace osu.Game
 
             // tracks play so loud our samples can't keep up.
             // this adds a global reduction of track volume for the time being.
-            Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));
+            Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));
 
-            beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio);
+            beatmap = new OsuBindableBeatmap(defaultBeatmap);
 
             dependencies.CacheAs<IBindable<WorkingBeatmap>>(beatmap);
             dependencies.CacheAs(beatmap);
@@ -281,23 +281,10 @@ namespace osu.Game
 
         private class OsuBindableBeatmap : BindableBeatmap
         {
-            public OsuBindableBeatmap(WorkingBeatmap defaultValue, AudioManager audioManager)
-                : this(defaultValue)
-            {
-                RegisterAudioManager(audioManager);
-            }
-
             public OsuBindableBeatmap(WorkingBeatmap defaultValue)
                 : base(defaultValue)
             {
             }
-
-            public override BindableBeatmap GetBoundCopy()
-            {
-                var copy = new OsuBindableBeatmap(Default);
-                copy.BindTo(this);
-                return copy;
-            }
         }
 
         private class OsuUserInputManager : UserInputManager
diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs
index 514e75c31a..52b77604d9 100644
--- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs
+++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs
@@ -90,8 +90,8 @@ namespace osu.Game.Overlays.Changelog
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
-            sampleClick = audio.Sample.Get(@"UI/generic-select-soft");
-            sampleHover = audio.Sample.Get(@"UI/generic-hover-soft");
+            sampleClick = audio.Samples.Get(@"UI/generic-select-soft");
+            sampleHover = audio.Samples.Get(@"UI/generic-hover-soft");
         }
 
         protected override void OnActivated() => updateState();
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index 7d791b2a88..4a6d53b480 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -76,7 +76,7 @@ namespace osu.Game.Overlays
                 },
             };
 
-            sampleBack = audio.Sample.Get(@"UI/generic-select-soft");
+            sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
 
             header.Current.BindTo(Current);
 
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 66a6672ab1..86bbe91d35 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Linq;
+using System.Collections.Generic;
 using osu.Framework.Allocation;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
@@ -14,6 +15,7 @@ using osu.Game.Graphics;
 using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
 using osu.Game.Online.Chat;
 using osu.Game.Users;
 using osuTK;
@@ -201,6 +203,9 @@ namespace osu.Game.Overlays.Chat
 
             private Action startChatAction;
 
+            [Resolved]
+            private IAPIProvider api { get; set; }
+
             public MessageSender(User sender)
             {
                 this.sender = sender;
@@ -213,11 +218,21 @@ namespace osu.Game.Overlays.Chat
                 startChatAction = () => chatManager?.OpenPrivateChannel(sender);
             }
 
-            public MenuItem[] ContextMenuItems => new MenuItem[]
+            public MenuItem[] ContextMenuItems
             {
-                new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action),
-                new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction),
-            };
+                get
+                {
+                    List<MenuItem> items = new List<MenuItem>
+                    {
+                        new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action)
+                    };
+
+                    if (sender.Id != api.LocalUser.Value.Id)
+                        items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction));
+
+                    return items.ToArray();
+                }
+            }
         }
 
         private static readonly Color4[] username_colours =
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/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs
index 6d82db5603..1f15c773f4 100644
--- a/osu.Game/Overlays/MedalOverlay.cs
+++ b/osu.Game/Overlays/MedalOverlay.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Overlays
         [BackgroundDependencyLoader]
         private void load(OsuColour colours, TextureStore textures, AudioManager audio)
         {
-            getSample = audio.Sample.Get(@"MedalSplash/medal-get");
+            getSample = audio.Samples.Get(@"MedalSplash/medal-get");
             innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
 
             disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 0e37e800ca..b57e98d09e 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -59,8 +59,8 @@ namespace osu.Game.Overlays.Mods
             Ruleset.BindTo(ruleset);
             if (mods != null) SelectedMods.BindTo(mods);
 
-            sampleOn = audio.Sample.Get(@"UI/check-on");
-            sampleOff = audio.Sample.Get(@"UI/check-off");
+            sampleOn = audio.Samples.Get(@"UI/check-on");
+            sampleOff = audio.Samples.Get(@"UI/check-off");
         }
 
         protected override void LoadComplete()
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index ea3e1ca00c..d7b915efe3 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -350,7 +350,7 @@ namespace osu.Game.Overlays
                     direction = last > next ? TransformDirection.Prev : TransformDirection.Next;
                 }
 
-                current.Track.Completed -= currentTrackCompleted;
+                //current.Track.Completed -= currentTrackCompleted;
             }
 
             current = beatmap.NewValue;
diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
index 0fc1398f5d..b6b0e605d7 100644
--- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs
@@ -29,13 +29,11 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
 
         protected override void ShowMore()
         {
-            base.ShowMore();
-
             request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage);
             request.Success += sets => Schedule(() =>
             {
-                ShowMoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0);
-                ShowMoreLoading.Hide();
+                MoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0);
+                MoreButton.IsLoading = false;
 
                 if (!sets.Any() && VisiblePages == 1)
                 {
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index f2eb32c53b..6085b0bc05 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -24,13 +24,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
 
         protected override void ShowMore()
         {
-            base.ShowMore();
-
             request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage);
             request.Success += beatmaps => Schedule(() =>
             {
-                ShowMoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0);
-                ShowMoreLoading.Hide();
+                MoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0);
+                MoreButton.IsLoading = false;
 
                 if (!beatmaps.Any() && VisiblePages == 1)
                 {
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index 46c65b9db7..8639acfc94 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -7,20 +7,17 @@ using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics;
-using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
 using osu.Game.Online.API;
 using osu.Game.Rulesets;
 using osu.Game.Users;
 
 namespace osu.Game.Overlays.Profile.Sections
 {
-    public class PaginatedContainer : FillFlowContainer
+    public abstract class PaginatedContainer : FillFlowContainer
     {
         protected readonly FillFlowContainer ItemsContainer;
-        protected readonly OsuHoverContainer ShowMoreButton;
-        protected readonly LoadingAnimation ShowMoreLoading;
+        protected readonly ShowMoreButton MoreButton;
         protected readonly OsuSpriteText MissingText;
 
         protected int VisiblePages;
@@ -32,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections
         protected APIRequest RetrievalRequest;
         protected RulesetStore Rulesets;
 
-        public PaginatedContainer(Bindable<User> user, string header, string missing)
+        protected PaginatedContainer(Bindable<User> user, string header, string missing)
         {
             User.BindTo(user);
 
@@ -45,38 +42,27 @@ namespace osu.Game.Overlays.Profile.Sections
                 new OsuSpriteText
                 {
                     Text = header,
-                    Font = OsuFont.GetFont(size: 15, weight: FontWeight.Regular, italics: true),
+                    Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold),
                     Margin = new MarginPadding { Top = 10, Bottom = 10 },
                 },
                 ItemsContainer = new FillFlowContainer
                 {
                     AutoSizeAxes = Axes.Y,
                     RelativeSizeAxes = Axes.X,
-                    Margin = new MarginPadding { Bottom = 10 }
+                    Direction = FillDirection.Vertical,
+                    Spacing = new Vector2(0, 2),
                 },
-                ShowMoreButton = new OsuHoverContainer
+                MoreButton = new ShowMoreButton
                 {
+                    Anchor = Anchor.TopCentre,
+                    Origin = Anchor.TopCentre,
                     Alpha = 0,
+                    Margin = new MarginPadding { Top = 10 },
                     Action = ShowMore,
-                    AutoSizeAxes = Axes.Both,
-                    Anchor = Anchor.TopCentre,
-                    Origin = Anchor.TopCentre,
-                    Child = new OsuSpriteText
-                    {
-                        Font = OsuFont.GetFont(size: 14),
-                        Text = "show more",
-                        Padding = new MarginPadding { Vertical = 10, Horizontal = 15 },
-                    }
-                },
-                ShowMoreLoading = new LoadingAnimation
-                {
-                    Anchor = Anchor.TopCentre,
-                    Origin = Anchor.TopCentre,
-                    Size = new Vector2(14),
                 },
                 MissingText = new OsuSpriteText
                 {
-                    Font = OsuFont.GetFont(size: 14),
+                    Font = OsuFont.GetFont(size: 15),
                     Text = missing,
                     Alpha = 0,
                 },
@@ -97,16 +83,11 @@ namespace osu.Game.Overlays.Profile.Sections
         {
             VisiblePages = 0;
             ItemsContainer.Clear();
-            ShowMoreButton.Hide();
 
             if (e.NewValue != null)
                 ShowMore();
         }
 
-        protected virtual void ShowMore()
-        {
-            ShowMoreLoading.Show();
-            ShowMoreButton.Hide();
-        }
+        protected abstract void ShowMore();
     }
 }
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 470bed2854..a149cfa12e 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -1,7 +1,6 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Online.API.Requests;
 using osu.Game.Users;
@@ -9,6 +8,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Bindables;
+using osu.Framework.Graphics;
 
 namespace osu.Game.Overlays.Profile.Sections.Ranks
 {
@@ -31,8 +31,6 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
 
         protected override void ShowMore()
         {
-            base.ShowMore();
-
             request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage);
             request.Success += scores => Schedule(() =>
             {
@@ -41,8 +39,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
 
                 if (!scores.Any() && VisiblePages == 1)
                 {
-                    ShowMoreButton.Hide();
-                    ShowMoreLoading.Hide();
+                    MoreButton.Hide();
+                    MoreButton.IsLoading = false;
                     MissingText.Show();
                     return;
                 }
@@ -63,8 +61,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
                 LoadComponentsAsync(drawableScores, s =>
                 {
                     MissingText.Hide();
-                    ShowMoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0);
-                    ShowMoreLoading.Hide();
+                    MoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0);
+                    MoreButton.IsLoading = false;
 
                     ItemsContainer.AddRange(s);
                 });
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
index 4b4acb8fbc..b72aec7a44 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs
@@ -22,13 +22,11 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
 
         protected override void ShowMore()
         {
-            base.ShowMore();
-
             request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage);
             request.Success += activities => Schedule(() =>
             {
-                ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0);
-                ShowMoreLoading.Hide();
+                MoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0);
+                MoreButton.IsLoading = false;
 
                 if (!activities.Any() && VisiblePages == 1)
                 {
diff --git a/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs
new file mode 100644
index 0000000000..5ed546c62b
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/ShowMoreButton.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+using System.Collections.Generic;
+
+namespace osu.Game.Overlays.Profile.Sections
+{
+    public class ShowMoreButton : OsuHoverContainer
+    {
+        private const float fade_duration = 200;
+
+        private readonly Box background;
+        private readonly LoadingAnimation loading;
+        private readonly FillFlowContainer content;
+
+        protected override IEnumerable<Drawable> EffectTargets => new[] { background };
+
+        private bool isLoading;
+
+        public bool IsLoading
+        {
+            get => isLoading;
+            set
+            {
+                if (isLoading == value)
+                    return;
+
+                isLoading = value;
+
+                Enabled.Value = !isLoading;
+
+                if (value)
+                {
+                    loading.FadeIn(fade_duration, Easing.OutQuint);
+                    content.FadeOut(fade_duration, Easing.OutQuint);
+                }
+                else
+                {
+                    loading.FadeOut(fade_duration, Easing.OutQuint);
+                    content.FadeIn(fade_duration, Easing.OutQuint);
+                }
+            }
+        }
+
+        public ShowMoreButton()
+        {
+            AutoSizeAxes = Axes.Both;
+            Children = new Drawable[]
+            {
+                new CircularContainer
+                {
+                    Masking = true,
+                    Size = new Vector2(140, 30),
+                    Children = new Drawable[]
+                    {
+                        background = new Box
+                        {
+                            RelativeSizeAxes = Axes.Both,
+                        },
+                        content = new FillFlowContainer
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            AutoSizeAxes = Axes.Both,
+                            Direction = FillDirection.Horizontal,
+                            Spacing = new Vector2(7),
+                            Children = new Drawable[]
+                            {
+                                new ChevronIcon(),
+                                new OsuSpriteText
+                                {
+                                    Anchor = Anchor.Centre,
+                                    Origin = Anchor.Centre,
+                                    Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+                                    Text = "show more".ToUpper(),
+                                },
+                                new ChevronIcon(),
+                            }
+                        },
+                        loading = new LoadingAnimation
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            Size = new Vector2(12)
+                        },
+                    }
+                }
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colors)
+        {
+            IdleColour = colors.GreySeafoamDark;
+            HoverColour = colors.GreySeafoam;
+        }
+
+        protected override bool OnClick(ClickEvent e)
+        {
+            if (!Enabled.Value)
+                return false;
+
+            try
+            {
+                return base.OnClick(e);
+            }
+            finally
+            {
+                // run afterwards as this will disable this button.
+                IsLoading = true;
+            }
+        }
+
+        private class ChevronIcon : SpriteIcon
+        {
+            private const int bottom_margin = 2;
+            private const int icon_size = 8;
+
+            public ChevronIcon()
+            {
+                Anchor = Anchor.Centre;
+                Origin = Anchor.Centre;
+                Margin = new MarginPadding { Bottom = bottom_margin };
+                Size = new Vector2(icon_size);
+                Icon = FontAwesome.Solid.ChevronDown;
+            }
+
+            [BackgroundDependencyLoader]
+            private void load(OsuColour colors)
+            {
+                Colour = colors.Yellow;
+            }
+        }
+    }
+}
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/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/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs
index 7d48f619d9..badd1e0549 100644
--- a/osu.Game/Screens/Menu/Button.cs
+++ b/osu.Game/Screens/Menu/Button.cs
@@ -182,9 +182,9 @@ namespace osu.Game.Screens.Menu
         [BackgroundDependencyLoader]
         private void load(AudioManager audio)
         {
-            sampleHover = audio.Sample.Get(@"Menu/button-hover");
+            sampleHover = audio.Samples.Get(@"Menu/button-hover");
             if (!string.IsNullOrEmpty(sampleName))
-                sampleClick = audio.Sample.Get($@"Menu/{sampleName}");
+                sampleClick = audio.Samples.Get($@"Menu/{sampleName}");
         }
 
         protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 868d37d922..5aa244cc06 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -150,7 +150,7 @@ namespace osu.Game.Screens.Menu
 
             if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle);
 
-            sampleBack = audio.Sample.Get(@"Menu/button-back-select");
+            sampleBack = audio.Samples.Get(@"Menu/button-back-select");
         }
 
         private void onMulti()
diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs
index 2392d650a0..98a2fe8f13 100644
--- a/osu.Game/Screens/Menu/Intro.cs
+++ b/osu.Game/Screens/Menu/Intro.cs
@@ -76,8 +76,8 @@ namespace osu.Game.Screens.Menu
             introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]);
             track = introBeatmap.Track;
 
-            welcome = audio.Sample.Get(@"welcome");
-            seeya = audio.Sample.Get(@"seeya");
+            welcome = audio.Samples.Get(@"welcome");
+            seeya = audio.Samples.Get(@"seeya");
         }
 
         private const double delay_step_one = 2300;
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/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 4631f4e222..479b3d80b6 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -255,8 +255,8 @@ namespace osu.Game.Screens.Menu
         [BackgroundDependencyLoader]
         private void load(TextureStore textures, AudioManager audio)
         {
-            sampleClick = audio.Sample.Get(@"Menu/osu-logo-select");
-            sampleBeat = audio.Sample.Get(@"Menu/osu-logo-heartbeat");
+            sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
+            sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
 
             logo.Texture = textures.Get(@"Menu/logo");
             ripple.Texture = textures.Get(@"Menu/logo");
diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs
index 2a6074882d..73994fa369 100644
--- a/osu.Game/Screens/Multi/Match/Components/Header.cs
+++ b/osu.Game/Screens/Multi/Match/Components/Header.cs
@@ -137,7 +137,7 @@ namespace osu.Game.Screens.Multi.Match.Components
 
             private class BackgroundSprite : UpdateableBeatmapBackgroundSprite
             {
-                protected override double FadeDuration => 200;
+                protected override double TransformDuration => 200;
             }
         }
     }
diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index 155665e0d5..9e5c11e098 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -255,7 +255,7 @@ namespace osu.Game.Screens.Multi
 
                     if (!track.IsRunning)
                     {
-                        game.Audio.AddItemToList(track);
+                        game.Audio.AddItem(track);
                         track.Seek(Beatmap.Value.Metadata.PreviewTime);
                         track.Start();
                     }
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 9d53e43b80..f7b90e9966 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Screens
         [BackgroundDependencyLoader(true)]
         private void load(OsuGame osu, AudioManager audio)
         {
-            sampleExit = audio.Sample.Get(@"UI/screen-back");
+            sampleExit = audio.Samples.Get(@"UI/screen-back");
         }
 
         public virtual bool OnPressed(GlobalAction action)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 30214d1b9c..cf743ee4f7 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Screens.Play
             if (working == null)
                 return;
 
-            sampleRestart = audio.Sample.Get(@"Gameplay/restart");
+            sampleRestart = audio.Samples.Get(@"Gameplay/restart");
 
             mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
             showStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs
index e3c56e1c2c..4ecc15f22b 100644
--- a/osu.Game/Screens/Play/SkipOverlay.cs
+++ b/osu.Game/Screens/Play/SkipOverlay.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Screens.Play
                 colourNormal = colours.Yellow;
                 colourHover = colours.YellowDark;
 
-                sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection");
+                sampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
 
                 Children = new Drawable[]
                 {
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/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
index f1d6343e72..b906bd935c 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select.Carousel
                 }
             };
 
-            sampleHover = audio.Sample.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
+            sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
             hoverLayer.Colour = colours.Blue.Opacity(0.1f);
         }
 
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index fed1f7a944..6d5be607f4 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -242,9 +242,9 @@ namespace osu.Game.Screens.Select
 
             dialogOverlay = dialog;
 
-            sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty");
-            sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand");
-            SampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection");
+            sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty");
+            sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand");
+            SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
 
             Carousel.LoadBeatmapSetsFromManager(this.beatmaps);
 
@@ -580,9 +580,6 @@ namespace osu.Game.Screens.Select
 
             if (!track.IsRunning || restart)
             {
-                // Ensure the track is added to the TrackManager, since it is removed after the player finishes the map.
-                // Using AddItemToList rather than AddItem so that it doesn't attempt to register adjustment dependencies more than once.
-                Game.Audio.Track.AddItemToList(track);
                 track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
                 track.Restart();
             }
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index ea4a777b47..7b658f86d0 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
@@ -21,7 +22,7 @@ namespace osu.Game.Skinning
     {
         protected TextureStore Textures;
 
-        protected SampleManager Samples;
+        protected IResourceStore<SampleChannel> Samples;
 
         public LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager)
             : this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, storage), audioManager, "skin.ini")
@@ -38,10 +39,17 @@ namespace osu.Game.Skinning
             else
                 Configuration = new SkinConfiguration();
 
-            Samples = audioManager.GetSampleManager(storage);
+            Samples = audioManager.GetSampleStore(storage);
             Textures = new TextureStore(new TextureLoaderStore(storage));
         }
 
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+            Textures?.Dispose();
+            Samples?.Dispose();
+        }
+
         public override Drawable GetDrawableComponent(string componentName)
         {
             switch (componentName)
@@ -133,6 +141,8 @@ namespace osu.Game.Skinning
                 return path == null ? null : underlyingStore.GetStream(path);
             }
 
+            public IEnumerable<string> GetAvailableResources() => source.Files.Select(f => f.Filename);
+
             byte[] IResourceStore<byte[]>.Get(string name) => GetAsync(name).Result;
 
             public Task<byte[]> GetAsync(string name)
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/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index d6f3625be8..e88e088f5e 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Skinning
             {
                 var ch = loadChannel(s, skin.GetSample);
                 if (ch == null && allowFallback)
-                    ch = loadChannel(s, audio.Sample.Get);
+                    ch = loadChannel(s, audio.Samples.Get);
                 return ch;
             }).Where(c => c != null).ToArray();
         }
@@ -58,5 +58,13 @@ namespace osu.Game.Skinning
 
             return null;
         }
+
+        protected override void Dispose(bool isDisposing)
+        {
+            base.Dispose(isDisposing);
+
+            foreach (var c in channels)
+                c.Dispose();
+        }
     }
 }
diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
index c558275f62..0ef35879e3 100644
--- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
@@ -1,134 +1,30 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using System;
 using osu.Framework.Audio.Track;
 using osu.Framework.Graphics.Textures;
-using osu.Framework.Timing;
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
-using osuTK;
 
 namespace osu.Game.Tests.Beatmaps
 {
     public class TestWorkingBeatmap : WorkingBeatmap
     {
-        private readonly TrackVirtualManual track;
         private readonly IBeatmap beatmap;
 
-        /// <summary>
-        /// Create an instance which creates a <see cref="TestBeatmap"/> for the provided ruleset when requested.
-        /// </summary>
-        /// <param name="ruleset">The target ruleset.</param>
-        /// <param name="referenceClock">A clock which should be used instead of a stopwatch for virtual time progression.</param>
-        public TestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock)
-            : this(new TestBeatmap(ruleset), referenceClock)
-        {
-        }
-
         /// <summary>
         /// Create an instance which provides the <see cref="IBeatmap"/> when requested.
         /// </summary>
         /// <param name="beatmap">The beatmap</param>
-        /// <param name="referenceClock">An optional clock which should be used instead of a stopwatch for virtual time progression.</param>
-        public TestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock = null)
-            : base(beatmap.BeatmapInfo)
+        public TestWorkingBeatmap(IBeatmap beatmap)
+            : base(beatmap.BeatmapInfo, null)
         {
             this.beatmap = beatmap;
-
-            if (referenceClock != null)
-                track = new TrackVirtualManual(referenceClock);
         }
 
         protected override IBeatmap GetBeatmap() => beatmap;
+
         protected override Texture GetBackground() => null;
-        protected override Track GetTrack() => track;
 
-        /// <summary>
-        /// A virtual track which tracks a reference clock.
-        /// </summary>
-        public class TrackVirtualManual : Track
-        {
-            private readonly IFrameBasedClock referenceClock;
-
-            private readonly ManualClock clock = new ManualClock();
-
-            private bool running;
-
-            /// <summary>
-            /// Local offset added to the reference clock to resolve correct time.
-            /// </summary>
-            private double offset;
-
-            public TrackVirtualManual(IFrameBasedClock referenceClock)
-            {
-                this.referenceClock = referenceClock;
-                Length = double.PositiveInfinity;
-            }
-
-            public override bool Seek(double seek)
-            {
-                offset = MathHelper.Clamp(seek, 0, Length);
-                lastReferenceTime = null;
-
-                return offset == seek;
-            }
-
-            public override void Start()
-            {
-                running = true;
-            }
-
-            public override void Reset()
-            {
-                Seek(0);
-                base.Reset();
-            }
-
-            public override void Stop()
-            {
-                if (running)
-                {
-                    running = false;
-                    // on stopping, the current value should be transferred out of the clock, as we can no longer rely on
-                    // the referenceClock (which will still be counting time).
-                    offset = clock.CurrentTime;
-                    lastReferenceTime = null;
-                }
-            }
-
-            public override bool IsRunning => running;
-
-            private double? lastReferenceTime;
-
-            public override double CurrentTime => clock.CurrentTime;
-
-            protected override void UpdateState()
-            {
-                base.UpdateState();
-
-                if (running)
-                {
-                    double refTime = referenceClock.CurrentTime;
-
-                    if (!lastReferenceTime.HasValue)
-                    {
-                        // if the clock just started running, the current value should be transferred to the offset
-                        // (to zero the progression of time).
-                        offset -= refTime;
-                    }
-
-                    lastReferenceTime = refTime;
-                }
-
-                clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length);
-
-                if (CurrentTime >= Length)
-                {
-                    Stop();
-                    RaiseCompleted();
-                }
-            }
-        }
+        protected override Track GetTrack() => null;
     }
 }
diff --git a/osu.Game/Tests/Visual/AllPlayersTestScene.cs b/osu.Game/Tests/Visual/AllPlayersTestScene.cs
index 454fbe1222..b7d1979b0d 100644
--- a/osu.Game/Tests/Visual/AllPlayersTestScene.cs
+++ b/osu.Game/Tests/Visual/AllPlayersTestScene.cs
@@ -4,13 +4,10 @@
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Screens;
-using osu.Framework.Timing;
-using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual
 {
@@ -50,26 +47,20 @@ namespace osu.Game.Tests.Visual
 
         protected abstract void AddCheckSteps();
 
-        protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
-
-        protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock clock) =>
-            new TestWorkingBeatmap(beatmap, Clock);
-
-        private Player loadPlayerFor(RulesetInfo ri)
+        private Player loadPlayerFor(RulesetInfo rulesetInfo)
         {
-            Ruleset.Value = ri;
-            var r = ri.CreateInstance();
+            Ruleset.Value = rulesetInfo;
+            var ruleset = rulesetInfo.CreateInstance();
 
-            var beatmap = CreateBeatmap(r);
-            var working = CreateWorkingBeatmap(beatmap, Clock);
+            var working = CreateWorkingBeatmap(rulesetInfo);
 
             Beatmap.Value = working;
-            Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) };
+            Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
 
             Player?.Exit();
             Player = null;
 
-            Player = CreatePlayer(r);
+            Player = CreatePlayer(ruleset);
 
             LoadScreen(Player);
 
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index 14c0f0950f..75bbb3e110 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.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;
@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using osu.Framework.Allocation;
 using osu.Game.Rulesets;
 using osu.Game.Screens.Edit;
-using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual
 {
@@ -24,7 +23,7 @@ namespace osu.Game.Tests.Visual
         [BackgroundDependencyLoader]
         private void load()
         {
-            Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo, null);
+            Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
 
             LoadScreen(new Editor());
         }
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index 9b775fd498..c8798448ae 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -3,15 +3,22 @@
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+using System.Threading.Tasks;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
+using osu.Framework.Audio.Track;
 using osu.Framework.Bindables;
+using osu.Framework.Graphics.Textures;
 using osu.Framework.Platform;
 using osu.Framework.Testing;
+using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
 
 namespace osu.Game.Tests.Visual
 {
@@ -19,7 +26,7 @@ namespace osu.Game.Tests.Visual
     {
         [Cached(typeof(Bindable<WorkingBeatmap>))]
         [Cached(typeof(IBindable<WorkingBeatmap>))]
-        private readonly OsuTestBeatmap beatmap = new OsuTestBeatmap(new DummyWorkingBeatmap());
+        private OsuTestBeatmap beatmap;
 
         protected BindableBeatmap Beatmap => beatmap;
 
@@ -39,7 +46,12 @@ namespace osu.Game.Tests.Visual
         protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
         {
             // This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures
-            beatmap.Default = new DummyWorkingBeatmap(parent.Get<OsuGameBase>());
+            var working = new DummyWorkingBeatmap(parent.Get<AudioManager>(), parent.Get<TextureStore>());
+
+            beatmap = new OsuTestBeatmap(working)
+            {
+                Default = working
+            };
 
             return Dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
         }
@@ -49,11 +61,20 @@ namespace osu.Game.Tests.Visual
             localStorage = new Lazy<Storage>(() => new NativeStorage($"{GetType().Name}-{Guid.NewGuid()}"));
         }
 
-        [BackgroundDependencyLoader]
-        private void load(AudioManager audioManager, RulesetStore rulesets)
-        {
-            beatmap.SetAudioManager(audioManager);
+        [Resolved]
+        private AudioManager audio { get; set; }
 
+        protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
+
+        protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) =>
+            CreateWorkingBeatmap(CreateBeatmap(ruleset));
+
+        protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) =>
+            new ClockBackedTestWorkingBeatmap(beatmap, Clock, audio);
+
+        [BackgroundDependencyLoader]
+        private void load(RulesetStore rulesets)
+        {
             Ruleset.Value = rulesets.AvailableRulesets.First();
         }
 
@@ -61,7 +82,8 @@ namespace osu.Game.Tests.Visual
         {
             base.Dispose(isDisposing);
 
-            beatmap?.Value.Track.Stop();
+            if (beatmap?.Value.TrackLoaded == true)
+                beatmap.Value.Track.Stop();
 
             if (localStorage.IsValueCreated)
             {
@@ -78,6 +100,164 @@ namespace osu.Game.Tests.Visual
 
         protected override ITestSceneTestRunner CreateRunner() => new OsuTestSceneTestRunner();
 
+        public class ClockBackedTestWorkingBeatmap : TestWorkingBeatmap
+        {
+            private readonly Track track;
+
+            private readonly TrackVirtualStore store;
+
+            /// <summary>
+            /// Create an instance which creates a <see cref="TestBeatmap"/> for the provided ruleset when requested.
+            /// </summary>
+            /// <param name="ruleset">The target ruleset.</param>
+            /// <param name="referenceClock">A clock which should be used instead of a stopwatch for virtual time progression.</param>
+            /// <param name="audio">Audio manager. Required if a reference clock isn't provided.</param>
+            public ClockBackedTestWorkingBeatmap(RulesetInfo ruleset, IFrameBasedClock referenceClock, AudioManager audio)
+                : this(new TestBeatmap(ruleset), referenceClock, audio)
+            {
+            }
+
+            /// <summary>
+            /// Create an instance which provides the <see cref="IBeatmap"/> when requested.
+            /// </summary>
+            /// <param name="beatmap">The beatmap</param>
+            /// <param name="referenceClock">An optional clock which should be used instead of a stopwatch for virtual time progression.</param>
+            /// <param name="audio">Audio manager. Required if a reference clock isn't provided.</param>
+            /// <param name="length">The length of the returned virtual track.</param>
+            public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000)
+                : base(beatmap)
+            {
+                if (referenceClock != null)
+                {
+                    store = new TrackVirtualStore(referenceClock);
+                    audio.AddItem(store);
+                    track = store.GetVirtual(length);
+                }
+                else
+                    track = audio?.Tracks.GetVirtual(length);
+            }
+
+            public override void Dispose()
+            {
+                base.Dispose();
+                store?.Dispose();
+            }
+
+            protected override Track GetTrack() => track;
+
+            public class TrackVirtualStore : AudioCollectionManager<Track>, ITrackStore
+            {
+                private readonly IFrameBasedClock referenceClock;
+
+                public TrackVirtualStore(IFrameBasedClock referenceClock)
+                {
+                    this.referenceClock = referenceClock;
+                }
+
+                public Track Get(string name) => throw new NotImplementedException();
+
+                public Task<Track> GetAsync(string name) => throw new NotImplementedException();
+
+                public Stream GetStream(string name) => throw new NotImplementedException();
+
+                public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
+
+                public Track GetVirtual(double length = Double.PositiveInfinity)
+                {
+                    var track = new TrackVirtualManual(referenceClock) { Length = length };
+                    AddItem(track);
+                    return track;
+                }
+            }
+
+            /// <summary>
+            /// A virtual track which tracks a reference clock.
+            /// </summary>
+            public class TrackVirtualManual : Track
+            {
+                private readonly IFrameBasedClock referenceClock;
+
+                private readonly ManualClock clock = new ManualClock();
+
+                private bool running;
+
+                /// <summary>
+                /// Local offset added to the reference clock to resolve correct time.
+                /// </summary>
+                private double offset;
+
+                public TrackVirtualManual(IFrameBasedClock referenceClock)
+                {
+                    this.referenceClock = referenceClock;
+                    Length = double.PositiveInfinity;
+                }
+
+                public override bool Seek(double seek)
+                {
+                    offset = MathHelper.Clamp(seek, 0, Length);
+                    lastReferenceTime = null;
+
+                    return offset == seek;
+                }
+
+                public override void Start()
+                {
+                    running = true;
+                }
+
+                public override void Reset()
+                {
+                    Seek(0);
+                    base.Reset();
+                }
+
+                public override void Stop()
+                {
+                    if (running)
+                    {
+                        running = false;
+                        // on stopping, the current value should be transferred out of the clock, as we can no longer rely on
+                        // the referenceClock (which will still be counting time).
+                        offset = clock.CurrentTime;
+                        lastReferenceTime = null;
+                    }
+                }
+
+                public override bool IsRunning => running;
+
+                private double? lastReferenceTime;
+
+                public override double CurrentTime => clock.CurrentTime;
+
+                protected override void UpdateState()
+                {
+                    base.UpdateState();
+
+                    if (running)
+                    {
+                        double refTime = referenceClock.CurrentTime;
+
+                        if (!lastReferenceTime.HasValue)
+                        {
+                            // if the clock just started running, the current value should be transferred to the offset
+                            // (to zero the progression of time).
+                            offset -= refTime;
+                        }
+
+                        lastReferenceTime = refTime;
+                    }
+
+                    clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length);
+
+                    if (CurrentTime >= Length)
+                    {
+                        Stop();
+                        RaiseCompleted();
+                    }
+                }
+            }
+        }
+
         public class OsuTestSceneTestRunner : OsuGameBase, ITestSceneTestRunner
         {
             private TestSceneTestRunner.TestRunner runner;
@@ -99,15 +279,6 @@ namespace osu.Game.Tests.Visual
                 : base(defaultValue)
             {
             }
-
-            public void SetAudioManager(AudioManager audioManager) => RegisterAudioManager(audioManager);
-
-            public override BindableBeatmap GetBoundCopy()
-            {
-                var copy = new OsuTestBeatmap(Default);
-                copy.BindTo(this);
-                return copy;
-            }
         }
     }
 }
diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
index c1561ffea1..2b177e264f 100644
--- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
+++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs
@@ -15,19 +15,18 @@ namespace osu.Game.Tests.Visual
     [Cached(Type = typeof(IPlacementHandler))]
     public abstract class PlacementBlueprintTestScene : OsuTestScene, IPlacementHandler
     {
-        protected readonly Container HitObjectContainer;
+        protected Container HitObjectContainer;
         private PlacementBlueprint currentBlueprint;
 
         protected PlacementBlueprintTestScene()
         {
-            Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2;
-
             Add(HitObjectContainer = CreateHitObjectContainer());
         }
 
         [BackgroundDependencyLoader]
         private void load()
         {
+            Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2;
             Add(currentBlueprint = CreateBlueprint());
         }
 
diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs
index 0c39194088..03e17a819c 100644
--- a/osu.Game/Tests/Visual/PlayerTestScene.cs
+++ b/osu.Game/Tests/Visual/PlayerTestScene.cs
@@ -4,12 +4,10 @@
 using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Testing;
-using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.Play;
-using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Tests.Visual
 {
@@ -39,15 +37,13 @@ namespace osu.Game.Tests.Visual
             AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
         }
 
-        protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
-
         protected virtual bool AllowFail => false;
 
         private void loadPlayer()
         {
-            var beatmap = CreateBeatmap(ruleset);
+            var beatmap = CreateBeatmap(ruleset.RulesetInfo);
 
-            Beatmap.Value = new TestWorkingBeatmap(beatmap, Clock);
+            Beatmap.Value = CreateWorkingBeatmap(beatmap);
 
             if (!AllowFail)
                 Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index b77c724d1b..55fa20188c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -15,7 +15,7 @@
     <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.523.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2019.606.1" />
     <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..68f21df8ba 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -105,8 +105,8 @@
     <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.Framework" Version="2019.606.1" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.606.1" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />