diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index a69070e93e..af8206d95a 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.UI;
 using System.Collections.Generic;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Catch.Objects;
 using osu.Game.Rulesets.Catch.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
@@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Catch
                         new MultiMod(new CatchModAutoplay(), new ModCinema()),
                         new CatchModRelax(),
                     };
+                case ModType.Fun:
+                    return new Mod[]
+                    {
+                        new MultiMod(new ModWindUp<CatchHitObject>(), new ModWindDown<CatchHitObject>())
+                    };
                 default:
                     return new Mod[] { };
             }
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 612df5bde5..692e63fa69 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
 {
     public class CatchModAutoplay : ModAutoplay<CatchHitObject>
     {
-        protected override Score CreateReplayScore(Beatmap<CatchHitObject> beatmap) => new Score
+        public override Score CreateReplayScore(IBeatmap beatmap) => new Score
         {
             ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
             Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 2fd05483ef..daa3f61de3 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -6,17 +6,20 @@ using System.Linq;
 using osu.Framework.MathUtils;
 using osu.Game.Beatmaps;
 using osu.Game.Replays;
+using osu.Game.Rulesets.Catch.Beatmaps;
 using osu.Game.Rulesets.Catch.Objects;
 using osu.Game.Rulesets.Catch.UI;
 using osu.Game.Rulesets.Replays;
 
 namespace osu.Game.Rulesets.Catch.Replays
 {
-    internal class CatchAutoGenerator : AutoGenerator<CatchHitObject>
+    internal class CatchAutoGenerator : AutoGenerator
     {
         public const double RELEASE_DELAY = 20;
 
-        public CatchAutoGenerator(Beatmap<CatchHitObject> beatmap)
+        public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
+
+        public CatchAutoGenerator(IBeatmap beatmap)
             : base(beatmap)
         {
             Replay = new Replay();
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index b40093844b..2b6b7377ae 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@ using System.Linq;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Bindings;
 using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
@@ -145,6 +146,11 @@ namespace osu.Game.Rulesets.Mania
                     {
                         new MultiMod(new ManiaModAutoplay(), new ModCinema()),
                     };
+                case ModType.Fun:
+                    return new Mod[]
+                    {
+                        new MultiMod(new ModWindUp<ManiaHitObject>(), new ModWindDown<ManiaHitObject>())
+                    };
                 default:
                     return new Mod[] { };
             }
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index 02eb7ac883..c05e979e9a 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
 {
     public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
     {
-        protected override Score CreateReplayScore(Beatmap<ManiaHitObject> beatmap) => new Score
+        public override Score CreateReplayScore(IBeatmap beatmap) => new Score
         {
             ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
             Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index df6afa040e..65b7d54cd2 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,13 +5,12 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Game.Replays;
 using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Replays;
 
 namespace osu.Game.Rulesets.Mania.Replays
 {
-    internal class ManiaAutoGenerator : AutoGenerator<ManiaHitObject>
+    internal class ManiaAutoGenerator : AutoGenerator
     {
         public const double RELEASE_DELAY = 20;
 
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
index 3d553c334f..5c1e775c01 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
@@ -16,18 +16,18 @@ namespace osu.Game.Rulesets.Osu.Tests
     [TestFixture]
     public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor
     {
-        private GameplayCursor cursor;
+        private GameplayCursorContainer cursorContainer;
 
         public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) };
 
-        public CursorContainer Cursor => cursor;
+        public CursorContainer Cursor => cursorContainer;
 
         public bool ProvidingUserCursor => true;
 
         [BackgroundDependencyLoader]
         private void load()
         {
-            Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both });
+            Add(cursorContainer = new GameplayCursorContainer { RelativeSizeAxes = Axes.Both });
         }
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
index c293a5a4f8..7886a2393c 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.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.Cursor;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Rulesets.UI;
@@ -16,8 +15,14 @@ namespace osu.Game.Rulesets.Osu.Edit
         {
         }
 
-        protected override CursorContainer CreateCursor() => null;
+        protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor { Size = Vector2.One };
 
-        protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One };
+        private class OsuPlayfieldNoCursor : OsuPlayfield
+        {
+            public OsuPlayfieldNoCursor()
+            {
+                Cursor?.Expire();
+            }
+        }
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index d7ba0d4da9..bea2bbcb32 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
     {
         public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
 
-        protected override Score CreateReplayScore(Beatmap<OsuHitObject> beatmap) => new Score
+        public override Score CreateReplayScore(IBeatmap beatmap) => new Score
         {
             ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
             Replay = new OsuAutoGenerator(beatmap).Generate()
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 20752517d5..d9c046a579 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -13,6 +13,7 @@ using osu.Game.Overlays.Settings;
 using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.Osu.Edit;
 using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
@@ -128,7 +129,8 @@ namespace osu.Game.Rulesets.Osu
                     {
                         new OsuModTransform(),
                         new OsuModWiggle(),
-                        new OsuModGrow()
+                        new OsuModGrow(),
+                        new MultiMod(new ModWindUp<OsuHitObject>(), new ModWindDown<OsuHitObject>()),
                     };
                 default:
                     return new Mod[] { };
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index b0fb85d7ed..c1aaa7767e 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -10,12 +10,15 @@ using System.Linq;
 using osu.Framework.Graphics;
 using osu.Game.Replays;
 using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Osu.Replays
 {
     public class OsuAutoGenerator : OsuAutoGeneratorBase
     {
+        public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
+
         #region Parameters
 
         /// <summary>
@@ -42,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Replays
 
         #region Construction / Initialisation
 
-        public OsuAutoGenerator(Beatmap<OsuHitObject> beatmap)
+        public OsuAutoGenerator(IBeatmap beatmap)
             : base(beatmap)
         {
             // Already superhuman, but still somewhat realistic
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 9a60f0cafc..9ab358ee12 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -3,7 +3,6 @@
 
 using osuTK;
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
 using System;
 using System.Collections.Generic;
 using osu.Game.Replays;
@@ -12,7 +11,7 @@ using osu.Game.Rulesets.Replays;
 
 namespace osu.Game.Rulesets.Osu.Replays
 {
-    public abstract class OsuAutoGeneratorBase : AutoGenerator<OsuHitObject>
+    public abstract class OsuAutoGeneratorBase : AutoGenerator
     {
         #region Constants
 
@@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Replays
         protected Replay Replay;
         protected List<ReplayFrame> Frames => Replay.Frames;
 
-        protected OsuAutoGeneratorBase(Beatmap<OsuHitObject> beatmap)
+        protected OsuAutoGeneratorBase(IBeatmap beatmap)
             : base(beatmap)
         {
             Replay = new Replay();
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
similarity index 97%
rename from osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs
rename to osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
index ef126cdf7d..8c6723f5be 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
@@ -17,7 +17,7 @@ using osuTK.Graphics;
 
 namespace osu.Game.Rulesets.Osu.UI.Cursor
 {
-    public class GameplayCursor : CursorContainer, IKeyBindingHandler<OsuAction>
+    public class GameplayCursorContainer : CursorContainer, IKeyBindingHandler<OsuAction>
     {
         protected override Drawable CreateCursor() => new OsuCursor();
 
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
 
         private readonly Container<Drawable> fadeContainer;
 
-        public GameplayCursor()
+        public GameplayCursorContainer()
         {
             InternalChild = fadeContainer = new Container
             {
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
             public OsuCursor()
             {
                 Origin = Anchor.Centre;
-                Size = new Vector2(42);
+                Size = new Vector2(28);
             }
 
             protected override void SkinChanged(ISkinSource skin, bool allowFallback)
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2db2b45540..51733c3c01 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -10,7 +10,9 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
 using osu.Game.Rulesets.UI;
 using System.Linq;
+using osu.Framework.Graphics.Cursor;
 using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.UI.Cursor;
 
 namespace osu.Game.Rulesets.Osu.UI
 {
@@ -22,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI
 
         public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
 
+        private readonly PlayfieldAdjustmentContainer adjustmentContainer;
+
+        protected override Container CursorTargetContainer => adjustmentContainer;
+
+        protected override CursorContainer CreateCursor() => new GameplayCursorContainer();
+
         public OsuPlayfield()
         {
             Anchor = Anchor.Centre;
@@ -29,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
 
             Size = new Vector2(0.75f);
 
-            InternalChild = new PlayfieldAdjustmentContainer
+            InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer
             {
                 RelativeSizeAxes = Axes.Both,
                 Children = new Drawable[]
diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
index 85b72cbb5b..81482a9a01 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System.Linq;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Input;
 using osu.Game.Beatmaps;
 using osu.Game.Input.Handlers;
@@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Replays;
 using osu.Game.Rulesets.Osu.Scoring;
-using osu.Game.Rulesets.Osu.UI.Cursor;
 using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.UI;
 
@@ -59,7 +57,5 @@ namespace osu.Game.Rulesets.Osu.UI
                 return first.StartTime - first.TimePreempt;
             }
         }
-
-        protected override CursorContainer CreateCursor() => new GameplayCursor();
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
index 4e5491da9c..5b890b3d03 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
 {
     public class TaikoModAutoplay : ModAutoplay<TaikoHitObject>
     {
-        protected override Score CreateReplayScore(Beatmap<TaikoHitObject> beatmap) => new Score
+        public override Score CreateReplayScore(IBeatmap beatmap) => new Score
         {
             ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
             Replay = new TaikoAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
index 14a6f98480..01ba53e07b 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
@@ -9,14 +9,17 @@ using osu.Game.Replays;
 using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Taiko.Objects;
 using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Taiko.Beatmaps;
 
 namespace osu.Game.Rulesets.Taiko.Replays
 {
-    public class TaikoAutoGenerator : AutoGenerator<TaikoHitObject>
+    public class TaikoAutoGenerator : AutoGenerator
     {
+        public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap;
+
         private const double swell_hit_speed = 50;
 
-        public TaikoAutoGenerator(Beatmap<TaikoHitObject> beatmap)
+        public TaikoAutoGenerator(IBeatmap beatmap)
             : base(beatmap)
         {
             Replay = new Replay();
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 7851a2f919..08a56488aa 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -11,6 +11,7 @@ using System.Collections.Generic;
 using osu.Framework.Graphics;
 using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.Replays.Types;
+using osu.Game.Rulesets.Taiko.Objects;
 using osu.Game.Rulesets.Taiko.Replays;
 using osu.Game.Beatmaps.Legacy;
 using osu.Game.Rulesets.Difficulty;
@@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko
                         new MultiMod(new TaikoModAutoplay(), new ModCinema()),
                         new TaikoModRelax(),
                     };
+                case ModType.Fun:
+                    return new Mod[]
+                    {
+                        new MultiMod(new ModWindUp<TaikoHitObject>(), new ModWindDown<TaikoHitObject>())
+                    };
                 default:
                     return new Mod[] { };
             }
diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs
index c12015a019..c34190d567 100644
--- a/osu.Game.Tests/Visual/TestCaseReplay.cs
+++ b/osu.Game.Tests/Visual/TestCaseReplay.cs
@@ -1,10 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using System.ComponentModel;
 using System.Linq;
 using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
 using osu.Game.Screens.Play;
 
 namespace osu.Game.Tests.Visual
@@ -14,15 +16,27 @@ namespace osu.Game.Tests.Visual
     {
         protected override Player CreatePlayer(Ruleset ruleset)
         {
-            // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here
-            // to simulate setting a replay rather than having the replay already set for us
-            Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
-            var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(Beatmap.Value);
+            var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo);
 
-            // Reset the mods
-            Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay));
+            return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
+        }
 
-            return new ReplayPlayer(dummyRulesetContainer.ReplayScore);
+        protected override void AddCheckSteps(Func<Player> player)
+        {
+            base.AddCheckSteps(player);
+            AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).ScoreProcessor.TotalScore.Value > 0, "score above zero");
+            AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys");
+        }
+
+        private class ScoreAccessibleReplayPlayer : ReplayPlayer
+        {
+            public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+            public new HUDOverlay HUDOverlay => base.HUDOverlay;
+
+            public ScoreAccessibleReplayPlayer(Score score)
+                : base(score)
+            {
+            }
         }
     }
 }
diff --git a/osu.Game.Tests/Visual/TestCaseToolbar.cs b/osu.Game.Tests/Visual/TestCaseToolbar.cs
index be1b75823a..cb5f33911b 100644
--- a/osu.Game.Tests/Visual/TestCaseToolbar.cs
+++ b/osu.Game.Tests/Visual/TestCaseToolbar.cs
@@ -24,10 +24,13 @@ namespace osu.Game.Tests.Visual
         public TestCaseToolbar()
         {
             var toolbar = new Toolbar { State = Visibility.Visible };
+            ToolbarNotificationButton notificationButton = null;
 
-            Add(toolbar);
-
-            var notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First();
+            AddStep("create toolbar", () =>
+            {
+                Add(toolbar);
+                notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First();
+            });
 
             void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);
 
diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
index 3ee617e092..506121efd7 100644
--- a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual
 {
     public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase
     {
-        private UpdateableBeatmapBackgroundSprite backgroundSprite;
+        private TestUpdateableBeatmapBackgroundSprite backgroundSprite;
 
         [Resolved]
         private BeatmapManager beatmaps { get; set; }
@@ -28,30 +28,36 @@ namespace osu.Game.Tests.Visual
 
             var imported = ImportBeatmapTest.LoadOszIntoOsu(osu);
 
-            Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
+            Child = backgroundSprite = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
 
             backgroundSprite.Beatmap.BindTo(beatmapBindable);
 
             var req = new GetBeatmapSetRequest(1);
             api.Queue(req);
 
-            AddStep("null", () => beatmapBindable.Value = null);
-
-            AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First());
+            AddStep("load null beatmap", () => beatmapBindable.Value = null);
+            AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
+            AddStep("load imported beatmap", () => beatmapBindable.Value = imported.Beatmaps.First());
+            AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
 
             if (api.IsLoggedIn)
             {
                 AddUntilStep(() => req.Result != null, "wait for api response");
-
-                AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
+                AddStep("load online beatmap", () => beatmapBindable.Value = new BeatmapInfo
                 {
                     BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
                 });
+                AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup...");
             }
             else
             {
                 AddStep("online (login first)", () => { });
             }
         }
+
+        private class TestUpdateableBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite
+        {
+            public int ChildCount => InternalChildren.Count;
+        }
     }
 }
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
index 012926fb0e..f0af09459f 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers;
 namespace osu.Game.Beatmaps.Drawables
 {
     /// <summary>
-    /// Display a baetmap background from a local source, but fallback to online source if not available.
+    /// Display a beatmap background from a local source, but fallback to online source if not available.
     /// </summary>
     public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo>
     {
@@ -26,37 +26,51 @@ namespace osu.Game.Beatmaps.Drawables
             this.beatmapSetCoverType = beatmapSetCoverType;
         }
 
-        protected override Drawable CreateDrawable(BeatmapInfo model)
+        private BeatmapInfo lastModel;
+
+        protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad)
         {
             return new DelayedLoadUnloadWrapper(() =>
             {
-                Drawable drawable;
+                // 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 (lastModel == Beatmap.Value && Beatmap.Value != null)
+                    return CreateDrawable(Beatmap.Value);
 
-                var localBeatmap = beatmaps.GetWorkingBeatmap(model);
-
-                if (model?.BeatmapSet?.OnlineInfo != null)
-                    drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
-                else if (localBeatmap.BeatmapInfo.ID != 0)
-                {
-                    // Fall back to local background if one exists
-                    drawable = new BeatmapBackgroundSprite(localBeatmap);
-                }
-                else
-                {
-                    // Use the default background if somehow an online set does not exist and we don't have a local copy.
-                    drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
-                }
-
-                drawable.RelativeSizeAxes = Axes.Both;
-                drawable.Anchor = Anchor.Centre;
-                drawable.Origin = Anchor.Centre;
-                drawable.FillMode = FillMode.Fill;
-                drawable.OnLoadComplete = d => d.FadeInFromZero(400);
-
-                return drawable;
-            }, 500, 10000);
+                // 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;
+                return content;
+            }, timeBeforeLoad, 10000);
         }
 
-        protected override double FadeDuration => 0;
+        protected override Drawable CreateDrawable(BeatmapInfo model)
+        {
+            Drawable drawable;
+
+            var localBeatmap = beatmaps.GetWorkingBeatmap(model);
+
+            if (model?.BeatmapSet?.OnlineInfo != null)
+            {
+                drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
+            }
+            else if (localBeatmap.BeatmapInfo.ID != 0)
+            {
+                // Fall back to local background if one exists
+                drawable = new BeatmapBackgroundSprite(localBeatmap);
+            }
+            else
+            {
+                // Use the default background if somehow an online set does not exist and we don't have a local copy.
+                drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
+            }
+
+            drawable.RelativeSizeAxes = Axes.Both;
+            drawable.Anchor = Anchor.Centre;
+            drawable.Origin = Anchor.Centre;
+            drawable.FillMode = FillMode.Fill;
+            drawable.OnLoadComplete = d => d.FadeInFromZero(400);
+
+            return drawable;
+        }
     }
 }
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 4d039e0b8a..5593abf348 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -70,13 +70,15 @@ namespace osu.Game.Online.API
 
         internal new void Schedule(Action action) => base.Schedule(action);
 
+        /// <summary>
+        /// Register a component to receive API events.
+        /// Fires <see cref="IOnlineComponent.APIStateChanged"/> once immediately to ensure a correct state.
+        /// </summary>
+        /// <param name="component"></param>
         public void Register(IOnlineComponent component)
         {
-            Scheduler.Add(delegate
-            {
-                components.Add(component);
-                component.APIStateChanged(this, state);
-            });
+            Scheduler.Add(delegate { components.Add(component); });
+            component.APIStateChanged(this, state);
         }
 
         public void Unregister(IOnlineComponent component)
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 61b2014af8..dca0226499 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Toolbar
 
         public Action OnHome;
 
-        private readonly ToolbarUserArea userArea;
+        private ToolbarUserArea userArea;
 
         protected override bool BlockPositionalInput => false;
 
@@ -34,6 +34,13 @@ namespace osu.Game.Overlays.Toolbar
         private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
 
         public Toolbar()
+        {
+            RelativeSizeAxes = Axes.X;
+            Size = new Vector2(1, HEIGHT);
+        }
+
+        [BackgroundDependencyLoader(true)]
+        private void load(OsuGame osuGame)
         {
             Children = new Drawable[]
             {
@@ -76,13 +83,6 @@ namespace osu.Game.Overlays.Toolbar
                 }
             };
 
-            RelativeSizeAxes = Axes.X;
-            Size = new Vector2(1, HEIGHT);
-        }
-
-        [BackgroundDependencyLoader(true)]
-        private void load(OsuGame osuGame)
-        {
             StateChanged += visibility =>
             {
                 if (overlayActivationMode.Value == OverlayActivation.Disabled)
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index 22e70b446e..44c78f8436 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -14,10 +14,6 @@ namespace osu.Game.Rulesets.Mods
     public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToRulesetContainer<T>
         where T : HitObject
     {
-        protected virtual Score CreateReplayScore(Beatmap<T> beatmap) => new Score { Replay = new Replay() };
-
-        public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
-
         public virtual void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap));
     }
 
@@ -31,5 +27,9 @@ namespace osu.Game.Rulesets.Mods
         public override double ScoreMultiplier => 1;
         public bool AllowFail => false;
         public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
+
+        public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
+
+        public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
     }
 }
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index b69019cd91..e59654c60d 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
         public override ModType Type => ModType.DifficultyIncrease;
         public override string Description => "Zoooooooooom...";
         public override bool Ranked => true;
-        public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) };
+        public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime), typeof(ModTimeRamp) };
 
         public virtual void ApplyToClock(IAdjustableClock clock)
         {
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 1cffa37b04..07cceb6f49 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
         public override ModType Type => ModType.DifficultyReduction;
         public override string Description => "Less zoom...";
         public override bool Ranked => true;
-        public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) };
+        public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModTimeRamp) };
 
         public virtual void ApplyToClock(IAdjustableClock clock)
         {
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
new file mode 100644
index 0000000000..4a0ed0f9bb
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Audio;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mods
+{
+    public abstract class ModTimeRamp : Mod
+    {
+        public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModHalfTime) };
+
+        protected abstract double FinalRateAdjustment { get; }
+    }
+
+    public abstract class ModTimeRamp<T> : ModTimeRamp, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap<T>
+        where T : HitObject
+    {
+        private double finalRateTime;
+
+        private double beginRampTime;
+
+        private IAdjustableClock clock;
+
+        private IHasPitchAdjust pitchAdjust;
+
+        /// <summary>
+        /// The point in the beatmap at which the final ramping rate should be reached.
+        /// </summary>
+        private const double final_rate_progress = 0.75f;
+
+        public virtual void ApplyToClock(IAdjustableClock clock)
+        {
+            this.clock = clock;
+            pitchAdjust = (IHasPitchAdjust)clock;
+
+            // for preview purposes
+            pitchAdjust.PitchAdjust = 1.0 + FinalRateAdjustment;
+        }
+
+        public virtual void ApplyToBeatmap(Beatmap<T> beatmap)
+        {
+            HitObject lastObject = beatmap.HitObjects.LastOrDefault();
+
+            beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
+            finalRateTime = final_rate_progress * ((lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0);
+        }
+
+        public virtual void Update(Playfield playfield)
+        {
+            var absRate = Math.Abs(FinalRateAdjustment);
+            var adjustment = MathHelper.Clamp(absRate * ((clock.CurrentTime - beginRampTime) / finalRateTime), 0, absRate);
+
+            pitchAdjust.PitchAdjust = 1 + Math.Sign(FinalRateAdjustment) * adjustment;
+        }
+    }
+}
diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
new file mode 100644
index 0000000000..646c5c64e4
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mods
+{
+    public class ModWindDown<T> : ModTimeRamp<T>
+        where T : HitObject
+    {
+        public override string Name => "Wind Down";
+        public override string Acronym => "WD";
+        public override string Description => "Sloooow doooown...";
+        public override FontAwesome Icon => FontAwesome.fa_chevron_circle_down;
+        public override double ScoreMultiplier => 1.0;
+        protected override double FinalRateAdjustment => -0.25;
+    }
+}
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
new file mode 100644
index 0000000000..9050b5591a
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mods
+{
+    public class ModWindUp<T> : ModTimeRamp<T>
+        where T : HitObject
+    {
+        public override string Name => "Wind Up";
+        public override string Acronym => "WU";
+        public override string Description => "Can you keep up?";
+        public override FontAwesome Icon => FontAwesome.fa_chevron_circle_up;
+        public override double ScoreMultiplier => 1.0;
+        protected override double FinalRateAdjustment => 0.5;
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Rulesets/Replays/AutoGenerator.cs b/osu.Game/Rulesets/Replays/AutoGenerator.cs
index efca0a5883..1d4cdbf04c 100644
--- a/osu.Game/Rulesets/Replays/AutoGenerator.cs
+++ b/osu.Game/Rulesets/Replays/AutoGenerator.cs
@@ -1,14 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
-using osu.Game.Rulesets.Objects;
 using osu.Game.Beatmaps;
 using osu.Game.Replays;
 
 namespace osu.Game.Rulesets.Replays
 {
-    public abstract class AutoGenerator<T> : IAutoGenerator
-        where T : HitObject
+    public abstract class AutoGenerator : IAutoGenerator
     {
         /// <summary>
         /// Creates the auto replay and returns it.
@@ -21,11 +19,11 @@ namespace osu.Game.Rulesets.Replays
         /// <summary>
         /// The beatmap we're making.
         /// </summary>
-        protected Beatmap<T> Beatmap;
+        protected IBeatmap Beatmap;
 
         #endregion
 
-        protected AutoGenerator(Beatmap<T> beatmap)
+        protected AutoGenerator(IBeatmap beatmap)
         {
             Beatmap = beatmap;
         }
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index ffab0abebf..70f15b99bd 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets
         /// <returns>An enumerable of constructed <see cref="Mod"/>s</returns>
         public virtual IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods) => new Mod[] { };
 
-        public Mod GetAutoplayMod() => GetAllMods().First(mod => mod is ModAutoplay);
+        public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First();
 
         protected Ruleset(RulesetInfo rulesetInfo = null)
         {
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 3b8a7353c6..78d14a27e3 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mods;
 using osuTK;
@@ -63,6 +64,10 @@ namespace osu.Game.Rulesets.UI
         private void load(IBindable<WorkingBeatmap> beatmap)
         {
             this.beatmap = beatmap.Value;
+
+            Cursor = CreateCursor();
+            if (Cursor != null)
+                CursorTargetContainer.Add(Cursor);
         }
 
         /// <summary>
@@ -82,6 +87,23 @@ namespace osu.Game.Rulesets.UI
         /// <param name="h">The DrawableHitObject to remove.</param>
         public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
 
+        /// <summary>
+        /// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
+        /// </summary>
+        public CursorContainer Cursor { get; private set; }
+
+        /// <summary>
+        /// Provide an optional cursor which is to be used for gameplay.
+        /// If providing a cursor, <see cref="CursorTargetContainer"/> must also point to a valid target container.
+        /// </summary>
+        /// <returns>The cursor, or null if a cursor is not rqeuired.</returns>
+        protected virtual CursorContainer CreateCursor() => null;
+
+        /// <summary>
+        /// The target container to add the cursor after it is created.
+        /// </summary>
+        protected virtual Container CursorTargetContainer => null;
+
         /// <summary>
         /// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
         /// This does not add the <see cref="Playfield"/> to the draw hierarchy.
diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs
index 1c29cf4e2b..ed5f23dc7f 100644
--- a/osu.Game/Rulesets/UI/RulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/RulesetContainer.cs
@@ -16,7 +16,9 @@ using System.Linq;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics.Cursor;
 using osu.Framework.Input;
+using osu.Framework.Input.Events;
 using osu.Game.Configuration;
+using osu.Game.Graphics.Cursor;
 using osu.Game.Input.Handlers;
 using osu.Game.Overlays;
 using osu.Game.Replays;
@@ -32,7 +34,7 @@ namespace osu.Game.Rulesets.UI
     /// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead.
     /// </para>
     /// </summary>
-    public abstract class RulesetContainer : Container
+    public abstract class RulesetContainer : Container, IProvideCursor
     {
         /// <summary>
         /// The selected variant.
@@ -74,10 +76,11 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         public Container Overlays { get; protected set; }
 
-        /// <summary>
-        /// The cursor provided by this <see cref="RulesetContainer"/>. May be null if no cursor is provided.
-        /// </summary>
-        public readonly CursorContainer Cursor;
+        public CursorContainer Cursor => Playfield.Cursor;
+
+        public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value;
+
+        protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
 
         public readonly Ruleset Ruleset;
 
@@ -101,8 +104,6 @@ namespace osu.Game.Rulesets.UI
 
                 KeyBindingInputManager.UseParentInput = !paused.NewValue;
             };
-
-            Cursor = CreateCursor();
         }
 
         protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -259,9 +260,6 @@ namespace osu.Game.Rulesets.UI
                 Playfield
             });
 
-            if (Cursor != null)
-                KeyBindingInputManager.Add(Cursor);
-
             InternalChildren = new Drawable[]
             {
                 KeyBindingInputManager,
diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs
similarity index 100%
rename from osu.Game/Screens/Multi/Components/ParticipantCount.cs
rename to osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs
diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
index dd1e060125..7cbae611ea 100644
--- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
@@ -107,6 +107,14 @@ namespace osu.Game.Screens.Multi.Lounge
             Filter.Search.HoldFocus = false;
         }
 
+        public override void OnResuming(IScreen last)
+        {
+            base.OnResuming(last);
+
+            if (currentRoom.Value?.RoomID.Value == null)
+                currentRoom.Value = new Room();
+        }
+
         private void joinRequested(Room room)
         {
             processingOverlay.Show();
diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs
index 6a6a1f274c..e1592532a3 100644
--- a/osu.Game/Screens/Multi/Match/Components/Header.cs
+++ b/osu.Game/Screens/Multi/Match/Components/Header.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using System;
+using System.Linq;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Extensions.Color4Extensions;
@@ -13,6 +14,7 @@ using osu.Game.Beatmaps.Drawables;
 using osu.Game.Graphics;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Overlays.SearchableList;
+using osu.Game.Rulesets.Mods;
 using osu.Game.Screens.Multi.Components;
 using osu.Game.Screens.Play.HUD;
 using osuTK;
@@ -108,7 +110,7 @@ namespace osu.Game.Screens.Multi.Match.Components
                 },
             };
 
-            CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods, true);
+            CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods ?? Enumerable.Empty<Mod>(), true);
 
             beatmapButton.Action = () => RequestBeatmapSelection?.Invoke();
         }
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
new file mode 100644
index 0000000000..594df63420
--- /dev/null
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -0,0 +1,151 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Screens.Play
+{
+    /// <summary>
+    /// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> for children.
+    /// </summary>
+    public class GameplayClockContainer : Container
+    {
+        private readonly WorkingBeatmap beatmap;
+
+        /// <summary>
+        /// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
+        /// </summary>
+        private readonly IAdjustableClock sourceClock;
+
+        public readonly BindableBool IsPaused = new BindableBool();
+
+        /// <summary>
+        /// The decoupled clock used for gameplay. Should be used for seeks and clock control.
+        /// </summary>
+        private readonly DecoupleableInterpolatingFramedClock adjustableClock;
+
+        public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
+        {
+            Default = 1,
+            MinValue = 0.5,
+            MaxValue = 2,
+            Precision = 0.1,
+        };
+
+        /// <summary>
+        /// The final clock which is exposed to underlying components.
+        /// </summary>
+        [Cached]
+        private readonly GameplayClock gameplayClock;
+
+        private Bindable<double> userAudioOffset;
+
+        private readonly FramedOffsetClock offsetClock;
+
+        public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime)
+        {
+            this.beatmap = beatmap;
+
+            RelativeSizeAxes = Axes.Both;
+
+            sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
+
+            adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
+
+            adjustableClock.Seek(allowLeadIn
+                ? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
+                : gameplayStartTime);
+
+            adjustableClock.ProcessFrame();
+
+            // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
+            // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
+            var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
+
+            // the final usable gameplay clock with user-set offsets applied.
+            offsetClock = new FramedOffsetClock(platformOffsetClock);
+
+            // the clock to be exposed via DI to children.
+            gameplayClock = new GameplayClock(offsetClock);
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuConfigManager config)
+        {
+            userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
+            userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true);
+
+            UserPlaybackRate.ValueChanged += _ => updateRate();
+        }
+
+        public void Restart()
+        {
+            Task.Run(() =>
+            {
+                sourceClock.Reset();
+
+                Schedule(() =>
+                {
+                    adjustableClock.ChangeSource(sourceClock);
+                    updateRate();
+
+                    this.Delay(750).Schedule(() =>
+                    {
+                        if (!IsPaused.Value)
+                        {
+                            adjustableClock.Start();
+                        }
+                    });
+                });
+            });
+        }
+
+        public void Start()
+        {
+            // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
+            // This accounts for the audio clock source potentially taking time to enter a completely stopped state
+            adjustableClock.Seek(adjustableClock.CurrentTime);
+            adjustableClock.Start();
+        }
+
+        public void Seek(double time) => adjustableClock.Seek(time);
+
+        public void Stop() => adjustableClock.Stop();
+
+        public void ResetLocalAdjustments()
+        {
+            // In the case of replays, we may have changed the playback rate.
+            UserPlaybackRate.Value = 1;
+        }
+
+        protected override void Update()
+        {
+            if (!IsPaused.Value)
+                offsetClock.ProcessFrame();
+
+            base.Update();
+        }
+
+        private void updateRate()
+        {
+            if (sourceClock == null) return;
+
+            sourceClock.Rate = 1;
+            foreach (var mod in beatmap.Mods.Value.OfType<IApplicableToClock>())
+                mod.ApplyToClock(sourceClock);
+
+            sourceClock.Rate *= UserPlaybackRate.Value;
+        }
+    }
+}
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index d3dba88281..e99f6d836e 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -19,7 +19,9 @@ namespace osu.Game.Screens.Play.HUD
         public readonly PlaybackSettings PlaybackSettings;
 
         public readonly VisualSettings VisualSettings;
+
         //public readonly CollectionSettings CollectionSettings;
+
         //public readonly DiscussionSettings DiscussionSettings;
 
         public PlayerSettingsOverlay()
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index a19b0d1e5c..4fd0572c1a 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -1,12 +1,12 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input.Events;
-using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Graphics.UserInterface;
@@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
 
         private static bool hasShownNotificationOnce;
 
-        public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock)
+        public Action<double> RequestSeek;
+
+        public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working)
         {
             RelativeSizeAxes = Axes.Both;
 
@@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
 
             Progress.Objects = rulesetContainer.Objects;
             Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
-            Progress.RequestSeek = pos => adjustableClock.Seek(pos);
+            Progress.RequestSeek = time => RequestSeek(time);
 
             ModDisplay.Current.BindTo(working.Mods);
-
-            PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
         }
 
         [BackgroundDependencyLoader(true)]
diff --git a/osu.Game/Screens/Play/PausableGameplayContainer.cs b/osu.Game/Screens/Play/PausableGameplayContainer.cs
index fa475deb34..99f0083b55 100644
--- a/osu.Game/Screens/Play/PausableGameplayContainer.cs
+++ b/osu.Game/Screens/Play/PausableGameplayContainer.cs
@@ -7,16 +7,13 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Timing;
 using osu.Game.Graphics;
 using osuTK.Graphics;
 
 namespace osu.Game.Screens.Play
 {
     /// <summary>
-    /// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
-    /// Exposes a <see cref="GameplayClock"/> to children via DI.
-    /// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
+    /// A container which handles pausing children, displaying an overlay blocking its children during paused state.
     /// </summary>
     public class PausableGameplayContainer : Container
     {
@@ -44,46 +41,33 @@ namespace osu.Game.Screens.Play
         public Action OnRetry;
         public Action OnQuit;
 
-        private readonly FramedClock offsetClock;
-        private readonly DecoupleableInterpolatingFramedClock adjustableClock;
-
-        /// <summary>
-        /// The final clock which is exposed to underlying components.
-        /// </summary>
-        [Cached]
-        private readonly GameplayClock gameplayClock;
+        public Action Stop;
+        public Action Start;
 
         /// <summary>
         /// Creates a new <see cref="PausableGameplayContainer"/>.
         /// </summary>
-        /// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
-        /// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
-        public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
+        public PausableGameplayContainer()
         {
-            this.offsetClock = offsetClock;
-            this.adjustableClock = adjustableClock;
-
-            gameplayClock = new GameplayClock(offsetClock);
-
             RelativeSizeAxes = Axes.Both;
 
-            AddInternal(content = new Container
+            InternalChildren = new[]
             {
-                Clock = this.offsetClock,
-                ProcessCustomClock = false,
-                RelativeSizeAxes = Axes.Both
-            });
-
-            AddInternal(pauseOverlay = new PauseOverlay
-            {
-                OnResume = () =>
+                content = new Container
                 {
-                    IsResuming = true;
-                    this.Delay(400).Schedule(Resume);
+                    RelativeSizeAxes = Axes.Both
                 },
-                OnRetry = () => OnRetry(),
-                OnQuit = () => OnQuit(),
-            });
+                pauseOverlay = new PauseOverlay
+                {
+                    OnResume = () =>
+                    {
+                        IsResuming = true;
+                        this.Delay(400).Schedule(Resume);
+                    },
+                    OnRetry = () => OnRetry(),
+                    OnQuit = () => OnQuit(),
+                }
+            };
         }
 
         public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
@@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play
             if (IsPaused.Value) return;
 
             // stop the seekable clock (stops the audio eventually)
-            adjustableClock.Stop();
+            Stop?.Invoke();
             IsPaused.Value = true;
 
             pauseOverlay.Show();
@@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
         {
             if (!IsPaused.Value) return;
 
-            IsPaused.Value = false;
             IsResuming = false;
             lastPauseActionTime = Time.Current;
 
-            // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
-            // This accounts for the audio clock source potentially taking time to enter a completely stopped state
-            adjustableClock.Seek(adjustableClock.CurrentTime);
-            adjustableClock.Start();
+            IsPaused.Value = false;
+
+            Start?.Invoke();
 
             pauseOverlay.Hide();
         }
@@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
             if (!game.IsActive.Value && CanPause)
                 Pause();
 
-            if (!IsPaused.Value)
-                offsetClock.ProcessFrame();
-
             base.Update();
         }
 
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 44707c74f5..ced0a43679 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -3,24 +3,19 @@
 
 using System;
 using System.Linq;
-using System.Threading.Tasks;
-using osu.Framework;
 using osu.Framework.Allocation;
 using osu.Framework.Audio;
 using osu.Framework.Audio.Sample;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
 using osu.Framework.Input.Events;
 using osu.Framework.Logging;
 using osu.Framework.Screens;
 using osu.Framework.Threading;
-using osu.Framework.Timing;
 using osu.Game.Beatmaps;
 using osu.Game.Configuration;
 using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Cursor;
 using osu.Game.Online.API;
 using osu.Game.Overlays;
 using osu.Game.Rulesets;
@@ -34,7 +29,7 @@ using osu.Game.Storyboards.Drawables;
 
 namespace osu.Game.Screens.Play
 {
-    public class Player : ScreenWithBeatmapBackground, IProvideCursor
+    public class Player : ScreenWithBeatmapBackground
     {
         protected override bool AllowBackButton => false; // handled by HoldForMenuButton
 
@@ -53,22 +48,11 @@ namespace osu.Game.Screens.Play
         public bool AllowResults { get; set; } = true;
 
         private Bindable<bool> mouseWheelDisabled;
-        private Bindable<double> userAudioOffset;
 
         private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
 
         public int RestartCount;
 
-        public CursorContainer Cursor => RulesetContainer.Cursor;
-        public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
-
-        private IAdjustableClock sourceClock;
-
-        /// <summary>
-        /// The decoupled clock used for gameplay. Should be used for seeks and clock control.
-        /// </summary>
-        private DecoupleableInterpolatingFramedClock adjustableClock;
-
         [Resolved]
         private ScoreManager scoreManager { get; set; }
 
@@ -98,25 +82,113 @@ namespace osu.Game.Screens.Play
 
         public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
 
+        private GameplayClockContainer gameplayClockContainer;
+
         [BackgroundDependencyLoader]
         private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
         {
             this.api = api;
 
-            WorkingBeatmap working = Beatmap.Value;
-            if (working is DummyWorkingBeatmap)
+            WorkingBeatmap working = loadBeatmap();
+
+            if (working == null)
                 return;
 
             sampleRestart = audio.Sample.Get(@"Gameplay/restart");
 
             mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
-            userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
 
-            IBeatmap beatmap;
+            ScoreProcessor = RulesetContainer.CreateScoreProcessor();
+            if (!ScoreProcessor.Mode.Disabled)
+                config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
+
+            InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, RulesetContainer.GameplayStartTime);
+
+            gameplayClockContainer.Children = new Drawable[]
+            {
+                PausableGameplayContainer = new PausableGameplayContainer
+                {
+                    Retries = RestartCount,
+                    OnRetry = restart,
+                    OnQuit = performUserRequestedExit,
+                    Start = gameplayClockContainer.Start,
+                    Stop = gameplayClockContainer.Stop,
+                    IsPaused = { BindTarget = gameplayClockContainer.IsPaused },
+                    CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
+                    Children = new[]
+                    {
+                        StoryboardContainer = CreateStoryboardContainer(),
+                        new ScalingContainer(ScalingMode.Gameplay)
+                        {
+                            Child = new LocalSkinOverrideContainer(working.Skin)
+                            {
+                                RelativeSizeAxes = Axes.Both,
+                                Child = RulesetContainer
+                            }
+                        },
+                        new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
+                        {
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre,
+                            Breaks = working.Beatmap.Breaks
+                        },
+                        // display the cursor above some HUD elements.
+                        RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
+                        HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working)
+                        {
+                            HoldToQuit = { Action = performUserRequestedExit },
+                            PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = gameplayClockContainer.UserPlaybackRate } } },
+                            KeyCounter = { Visible = { BindTarget = RulesetContainer.HasReplayLoaded } },
+                            RequestSeek = gameplayClockContainer.Seek,
+                            Anchor = Anchor.Centre,
+                            Origin = Anchor.Centre
+                        },
+                        new SkipOverlay(RulesetContainer.GameplayStartTime)
+                        {
+                            RequestSeek = gameplayClockContainer.Seek
+                        },
+                    }
+                },
+                failOverlay = new FailOverlay
+                {
+                    OnRetry = restart,
+                    OnQuit = performUserRequestedExit,
+                },
+                new HotkeyRetryOverlay
+                {
+                    Action = () =>
+                    {
+                        if (!this.IsCurrentScreen()) return;
+
+                        fadeOut(true);
+                        restart();
+                    },
+                }
+            };
+
+            // bind clock into components that require it
+            RulesetContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused);
+
+            if (ShowStoryboard.Value)
+                initializeStoryboard(false);
+
+            // Bind ScoreProcessor to ourselves
+            ScoreProcessor.AllJudged += onCompletion;
+            ScoreProcessor.Failed += onFail;
+
+            foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
+                mod.ApplyToScoreProcessor(ScoreProcessor);
+        }
+
+        private WorkingBeatmap loadBeatmap()
+        {
+            WorkingBeatmap working = Beatmap.Value;
+            if (working is DummyWorkingBeatmap)
+                return null;
 
             try
             {
-                beatmap = working.Beatmap;
+                var beatmap = working.Beatmap;
 
                 if (beatmap == null)
                     throw new InvalidOperationException("Beatmap was not loaded");
@@ -140,119 +212,17 @@ namespace osu.Game.Screens.Play
                 if (!RulesetContainer.Objects.Any())
                 {
                     Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
-                    return;
+                    return null;
                 }
             }
             catch (Exception e)
             {
                 Logger.Error(e, "Could not load beatmap sucessfully!");
                 //couldn't load, hard abort!
-                return;
+                return null;
             }
 
-            sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
-            adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
-
-            adjustableClock.Seek(AllowLeadIn
-                ? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
-                : RulesetContainer.GameplayStartTime);
-
-            adjustableClock.ProcessFrame();
-
-            // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
-            // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
-            var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
-
-            // the final usable gameplay clock with user-set offsets applied.
-            var offsetClock = new FramedOffsetClock(platformOffsetClock);
-
-            userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue;
-            userAudioOffset.TriggerChange();
-
-            ScoreProcessor = RulesetContainer.CreateScoreProcessor();
-            if (!ScoreProcessor.Mode.Disabled)
-                config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
-
-            InternalChildren = new Drawable[]
-            {
-                PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
-                {
-                    Retries = RestartCount,
-                    OnRetry = restart,
-                    OnQuit = performUserRequestedExit,
-                    CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
-                    Children = new Container[]
-                    {
-                        StoryboardContainer = CreateStoryboardContainer(),
-                        new ScalingContainer(ScalingMode.Gameplay)
-                        {
-                            Child = new LocalSkinOverrideContainer(working.Skin)
-                            {
-                                RelativeSizeAxes = Axes.Both,
-                                Child = RulesetContainer
-                            }
-                        },
-                        new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
-                        {
-                            Anchor = Anchor.Centre,
-                            Origin = Anchor.Centre,
-                            Breaks = beatmap.Breaks
-                        },
-                        new ScalingContainer(ScalingMode.Gameplay)
-                        {
-                            Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
-                        },
-                        HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
-                        {
-                            Anchor = Anchor.Centre,
-                            Origin = Anchor.Centre
-                        },
-                        new SkipOverlay(RulesetContainer.GameplayStartTime)
-                        {
-                            RequestSeek = time => adjustableClock.Seek(time)
-                        },
-                    }
-                },
-                failOverlay = new FailOverlay
-                {
-                    OnRetry = restart,
-                    OnQuit = performUserRequestedExit,
-                },
-                new HotkeyRetryOverlay
-                {
-                    Action = () =>
-                    {
-                        if (!this.IsCurrentScreen()) return;
-
-                        fadeOut(true);
-                        restart();
-                    },
-                }
-            };
-
-            HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
-            HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
-
-            RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
-
-            if (ShowStoryboard.Value)
-                initializeStoryboard(false);
-
-            // Bind ScoreProcessor to ourselves
-            ScoreProcessor.AllJudged += onCompletion;
-            ScoreProcessor.Failed += onFail;
-
-            foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
-                mod.ApplyToScoreProcessor(ScoreProcessor);
-        }
-
-        private void applyRateFromMods()
-        {
-            if (sourceClock == null) return;
-
-            sourceClock.Rate = 1;
-            foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>())
-                mod.ApplyToClock(sourceClock);
+            return working;
         }
 
         private void performUserRequestedExit()
@@ -321,7 +291,7 @@ namespace osu.Game.Screens.Play
             if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
                 return false;
 
-            adjustableClock.Stop();
+            gameplayClockContainer.Stop();
 
             HasFailed = true;
             failOverlay.Retries = RestartCount;
@@ -355,24 +325,7 @@ namespace osu.Game.Screens.Play
 
             storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
 
-            Task.Run(() =>
-            {
-                sourceClock.Reset();
-
-                Schedule(() =>
-                {
-                    adjustableClock.ChangeSource(sourceClock);
-                    applyRateFromMods();
-
-                    this.Delay(750).Schedule(() =>
-                    {
-                        if (!PausableGameplayContainer.IsPaused.Value)
-                        {
-                            adjustableClock.Start();
-                        }
-                    });
-                });
-            });
+            gameplayClockContainer.Restart();
 
             PausableGameplayContainer.Alpha = 0;
             PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
@@ -395,8 +348,8 @@ namespace osu.Game.Screens.Play
 
             if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
             {
-                // In the case of replays, we may have changed the playback rate.
-                applyRateFromMods();
+                gameplayClockContainer.ResetLocalAdjustments();
+
                 fadeOut();
                 return base.OnExiting(next);
             }
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
index ebbed299f7..c691d161ed 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Timing;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 
@@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
 
         protected override string Title => @"playback";
 
-        public IAdjustableClock AdjustableClock { set; get; }
+        public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
+        {
+            Default = 1,
+            MinValue = 0.5,
+            MaxValue = 2,
+            Precision = 0.1,
+        };
 
         private readonly PlayerSliderBar<double> rateSlider;
 
@@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
                         }
                     },
                 },
-                rateSlider = new PlayerSliderBar<double>
-                {
-                    Bindable = new BindableDouble(1)
-                    {
-                        Default = 1,
-                        MinValue = 0.5,
-                        MaxValue = 2,
-                        Precision = 0.1,
-                    },
-                }
+                rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
             };
         }
 
         protected override void LoadComplete()
         {
             base.LoadComplete();
-
-            if (AdjustableClock == null)
-                return;
-
-            var clockRate = AdjustableClock.Rate;
-
-            // can't trigger this line instantly as the underlying clock may not be ready to accept adjustments yet.
-            rateSlider.Bindable.ValueChanged += multiplier => AdjustableClock.Rate = clockRate * multiplier.NewValue;
-
             rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true);
         }
     }
diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs
index 3dc152eebe..fa5dc4c1d1 100644
--- a/osu.Game/Screens/Select/MatchSongSelect.cs
+++ b/osu.Game/Screens/Select/MatchSongSelect.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Humanizer;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -64,7 +65,7 @@ namespace osu.Game.Screens.Select
             {
                 Ruleset.Value = CurrentItem.Value.Ruleset;
                 Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap);
-                Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods;
+                Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods ?? Enumerable.Empty<Mod>();
             }
 
             Beatmap.Disabled = true;
diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs
index acc3ab28b5..7f4faa60ae 100644
--- a/osu.Game/Utils/RavenLogger.cs
+++ b/osu.Game/Utils/RavenLogger.cs
@@ -20,14 +20,14 @@ namespace osu.Game.Utils
 
         private readonly List<Task> tasks = new List<Task>();
 
-        private Exception lastException;
-
         public RavenLogger(OsuGame game)
         {
             raven.Release = game.Version;
 
             if (!game.IsDeployedBuild) return;
 
+            Exception lastException = null;
+
             Logger.NewEntry += entry =>
             {
                 if (entry.Level < LogLevel.Verbose) return;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 882c6ef064..2e945c212d 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -16,7 +16,7 @@
     <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.307.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />
diff --git a/osu.iOS.props b/osu.iOS.props
index bc3e553fef..b25e2a8bb2 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.307.0" />
-    <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.307.0" />
+    <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" />
+    <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.308.0" />
     <PackageReference Include="SharpCompress" Version="0.22.0" />
     <PackageReference Include="NUnit" Version="3.11.0" />
     <PackageReference Include="SharpRaven" Version="2.4.0" />