diff --git a/osu-framework b/osu-framework
index 74f644bad0..1ba1e8ef1e 160000
--- a/osu-framework
+++ b/osu-framework
@@ -1 +1 @@
-Subproject commit 74f644bad039606e242d8782014d8c249a38b6a3
+Subproject commit 1ba1e8ef1e5ec0466632be02492023a081cb85ab
diff --git a/osu.Desktop.Tests/Visual/OsuTestCase.cs b/osu.Desktop.Tests/Visual/OsuTestCase.cs
index e54f7dbeb5..e366aecb21 100644
--- a/osu.Desktop.Tests/Visual/OsuTestCase.cs
+++ b/osu.Desktop.Tests/Visual/OsuTestCase.cs
@@ -14,7 +14,7 @@ namespace osu.Desktop.Tests.Visual
         [Test]
         public override void RunTest()
         {
-            using (var host = new HeadlessGameHost())
+            using (var host = new HeadlessGameHost(realtime: false))
                 host.Run(new OsuTestCaseTestRunner(this));
         }
 
diff --git a/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs
index 7dcc48c778..76235bbf19 100644
--- a/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs
+++ b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs
@@ -3,6 +3,7 @@
 
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Mania;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Objects.Drawables;
 using OpenTK;
@@ -40,8 +41,8 @@ namespace osu.Desktop.Tests.Visual
                                 RelativeChildSize = new Vector2(1, 10000),
                                 Children = new[]
                                 {
-                                    new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red },
-                                    new DrawableNote(new Note { StartTime = 6000 }) { AccentColour = Color4.Red }
+                                    new DrawableNote(new Note { StartTime = 5000 }, ManiaAction.Key1) { AccentColour = Color4.Red },
+                                    new DrawableNote(new Note { StartTime = 6000 }, ManiaAction.Key1) { AccentColour = Color4.Red }
                                 }
                             }
                         }
@@ -66,7 +67,7 @@ namespace osu.Desktop.Tests.Visual
                                     {
                                         StartTime = 5000,
                                         Duration = 1000
-                                    }) { AccentColour = Color4.Red }
+                                    }, ManiaAction.Key1) { AccentColour = Color4.Red }
                                 }
                             }
                         }
diff --git a/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs
index ed0e5d81e9..c52594930e 100644
--- a/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs
+++ b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs
@@ -3,109 +3,34 @@
 
 using System;
 using System.Linq;
-using osu.Framework.Configuration;
+using osu.Framework.Allocation;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
-using osu.Framework.Input;
 using osu.Framework.Timing;
+using osu.Game.Rulesets.Mania;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Objects.Drawables;
 using osu.Game.Rulesets.Mania.Timing;
 using osu.Game.Rulesets.Mania.UI;
 using osu.Game.Rulesets.Timing;
 using OpenTK;
-using OpenTK.Input;
+using osu.Game.Rulesets;
 
 namespace osu.Desktop.Tests.Visual
 {
     internal class TestCaseManiaPlayfield : OsuTestCase
     {
+        private const double start_time = 500;
+        private const double duration = 500;
+
         public override string Description => @"Mania playfield";
 
         protected override double TimePerAction => 200;
 
+        private RulesetInfo maniaRuleset;
+
         public TestCaseManiaPlayfield()
         {
-            Action<int, SpecialColumnPosition> createPlayfield = (cols, pos) =>
-            {
-                Clear();
-                Add(new ManiaPlayfield(cols)
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    SpecialColumnPosition = pos,
-                    Scale = new Vector2(1, -1)
-                });
-            };
-
-            const double start_time = 500;
-            const double duration = 500;
-
-            Func<double, bool, SpeedAdjustmentContainer> createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time)
-            {
-                TimingPoint = { BeatLength = 1000 }
-            }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic);
-
-            Action<bool> createPlayfieldWithNotes = gravity =>
-            {
-                Clear();
-
-                var rateAdjustClock = new StopwatchClock(true) { Rate = 1 };
-
-                ManiaPlayfield playField;
-                Add(playField = new ManiaPlayfield(4)
-                {
-                    Anchor = Anchor.Centre,
-                    Origin = Anchor.Centre,
-                    Scale = new Vector2(1, -1),
-                    Clock = new FramedClock(rateAdjustClock)
-                });
-
-                if (!gravity)
-                    playField.Columns.ForEach(c => c.Add(createTimingChange(0, false)));
-
-                for (double t = start_time; t <= start_time + duration; t += 100)
-                {
-                    if (gravity)
-                        playField.Columns.ElementAt(0).Add(createTimingChange(t, true));
-
-                    playField.Add(new DrawableNote(new Note
-                    {
-                        StartTime = t,
-                        Column = 0
-                    }, new Bindable<Key>(Key.D)));
-
-                    if (gravity)
-                        playField.Columns.ElementAt(3).Add(createTimingChange(t, true));
-
-                    playField.Add(new DrawableNote(new Note
-                    {
-                        StartTime = t,
-                        Column = 3
-                    }, new Bindable<Key>(Key.K)));
-                }
-
-                if (gravity)
-                    playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true));
-
-                playField.Add(new DrawableHoldNote(new HoldNote
-                {
-                    StartTime = start_time,
-                    Duration = duration,
-                    Column = 1
-                }, new Bindable<Key>(Key.F)));
-
-                if (gravity)
-                    playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true));
-
-                playField.Add(new DrawableHoldNote(new HoldNote
-                {
-                    StartTime = start_time,
-                    Duration = duration,
-                    Column = 2
-                }, new Bindable<Key>(Key.J)));
-            };
-
             AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal));
             AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal));
             AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left));
@@ -122,21 +47,94 @@ namespace osu.Desktop.Tests.Visual
             AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction));
         }
 
-        private void triggerKeyDown(Column column)
+        [BackgroundDependencyLoader]
+        private void load(RulesetStore rulesets)
         {
-            column.TriggerOnKeyDown(new InputState(), new KeyDownEventArgs
+            maniaRuleset = rulesets.GetRuleset(3);
+        }
+
+        private SpeedAdjustmentContainer createTimingChange(double time, bool gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time)
+        {
+            TimingPoint = { BeatLength = 1000 }
+        }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic);
+
+        private void createPlayfield(int cols, SpecialColumnPosition specialPos)
+        {
+            Clear();
+
+            var inputManager = new ManiaInputManager(maniaRuleset, cols) { RelativeSizeAxes = Axes.Both };
+            Add(inputManager);
+
+            inputManager.Add(new ManiaPlayfield(cols)
             {
-                Key = column.Key,
-                Repeat = false
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                SpecialColumnPosition = specialPos,
+                Scale = new Vector2(1, -1)
             });
         }
 
-        private void triggerKeyUp(Column column)
+        private void createPlayfieldWithNotes(bool gravity)
         {
-            column.TriggerOnKeyUp(new InputState(), new KeyUpEventArgs
+            Clear();
+
+            var rateAdjustClock = new StopwatchClock(true) { Rate = 1 };
+
+            var inputManager = new ManiaInputManager(maniaRuleset, 4) { RelativeSizeAxes = Axes.Both };
+            Add(inputManager);
+
+            ManiaPlayfield playField;
+            inputManager.Add(playField = new ManiaPlayfield(4)
             {
-                Key = column.Key
+                Anchor = Anchor.Centre,
+                Origin = Anchor.Centre,
+                Scale = new Vector2(1, -1),
+                Clock = new FramedClock(rateAdjustClock)
             });
+
+            if (!gravity)
+                playField.Columns.ForEach(c => c.Add(createTimingChange(0, false)));
+
+            for (double t = start_time; t <= start_time + duration; t += 100)
+            {
+                if (gravity)
+                    playField.Columns.ElementAt(0).Add(createTimingChange(t, true));
+
+                playField.Add(new DrawableNote(new Note
+                {
+                    StartTime = t,
+                    Column = 0
+                }, ManiaAction.Key1));
+
+                if (gravity)
+                    playField.Columns.ElementAt(3).Add(createTimingChange(t, true));
+
+                playField.Add(new DrawableNote(new Note
+                {
+                    StartTime = t,
+                    Column = 3
+                }, ManiaAction.Key4));
+            }
+
+            if (gravity)
+                playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true));
+
+            playField.Add(new DrawableHoldNote(new HoldNote
+            {
+                StartTime = start_time,
+                Duration = duration,
+                Column = 1
+            }, ManiaAction.Key2));
+
+            if (gravity)
+                playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true));
+
+            playField.Add(new DrawableHoldNote(new HoldNote
+            {
+                StartTime = start_time,
+                Duration = duration,
+                Column = 2
+            }, ManiaAction.Key3));
         }
     }
 }
diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
index b608e4d8d6..a574bc75ec 100644
--- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs
+++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs
@@ -1,6 +1,7 @@
 // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
+using System.ComponentModel;
 using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.UI;
 
@@ -8,14 +9,33 @@ namespace osu.Game.Rulesets.Mania
 {
     public class ManiaInputManager : RulesetInputManager<ManiaAction>
     {
-        public ManiaInputManager(RulesetInfo ruleset)
-            : base(ruleset, 0, SimultaneousBindingMode.Unique)
+        public ManiaInputManager(RulesetInfo ruleset, int variant)
+            : base(ruleset, variant, SimultaneousBindingMode.Unique)
         {
         }
     }
 
     public enum ManiaAction
     {
-        // placeholder
+        [Description("Special")]
+        Special,
+        [Description("Key 1")]
+        Key1 = 10,
+        [Description("Key 2")]
+        Key2,
+        [Description("Key 3")]
+        Key3,
+        [Description("Key 4")]
+        Key4,
+        [Description("Key 5")]
+        Key5,
+        [Description("Key 6")]
+        Key6,
+        [Description("Key 7")]
+        Key7,
+        [Description("Key 8")]
+        Key8,
+        [Description("Key 9")]
+        Key9
     }
 }
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index c7809f83ed..ed46da4f85 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.UI;
 using System.Collections.Generic;
 using osu.Framework.Graphics;
+using osu.Framework.Input.Bindings;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Mania.Scoring;
 using osu.Game.Rulesets.Scoring;
@@ -120,5 +121,43 @@ namespace osu.Game.Rulesets.Mania
             : base(rulesetInfo)
         {
         }
+
+        public override IEnumerable<int> AvailableVariants => new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+        public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0)
+        {
+            var leftKeys = new[]
+            {
+                InputKey.A,
+                InputKey.S,
+                InputKey.D,
+                InputKey.F
+            };
+
+            var rightKeys = new[]
+            {
+                InputKey.J,
+                InputKey.K,
+                InputKey.L,
+                InputKey.Semicolon
+            };
+
+            ManiaAction currentKey = ManiaAction.Key1;
+
+            var bindings = new List<KeyBinding>();
+
+            for (int i = leftKeys.Length - variant / 2; i < leftKeys.Length; i++)
+                bindings.Add(new KeyBinding(leftKeys[i], currentKey++));
+
+            for (int i = 0; i < variant / 2; i++)
+                bindings.Add(new KeyBinding(rightKeys[i], currentKey++));
+
+            if (variant % 2 == 1)
+                bindings.Add(new KeyBinding(InputKey.Space, ManiaAction.Special));
+
+            return bindings;
+        }
+
+        public override string GetVariantName(int variant) => $"{variant}K";
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 17b0b0a607..e06f71cb64 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -5,20 +5,18 @@ using osu.Game.Rulesets.Objects.Drawables;
 using osu.Framework.Graphics;
 using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
 using OpenTK.Graphics;
-using osu.Framework.Configuration;
-using OpenTK.Input;
-using osu.Framework.Input;
 using OpenTK;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Rulesets.Mania.Judgements;
 using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Input.Bindings;
 
 namespace osu.Game.Rulesets.Mania.Objects.Drawables
 {
     /// <summary>
     /// Visualises a <see cref="HoldNote"/> hit object.
     /// </summary>
-    public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>
+    public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>, IKeyBindingHandler<ManiaAction>
     {
         private readonly DrawableNote head;
         private readonly DrawableNote tail;
@@ -36,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
         /// </summary>
         private bool hasBroken;
 
-        public DrawableHoldNote(HoldNote hitObject, Bindable<Key> key = null)
-            : base(hitObject, key)
+        public DrawableHoldNote(HoldNote hitObject, ManiaAction action)
+            : base(hitObject, action)
         {
             RelativeSizeAxes = Axes.Both;
             Height = (float)HitObject.Duration;
@@ -58,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
                     RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime),
                     RelativeChildSize = new Vector2(1, (float)HitObject.Duration)
                 },
-                head = new DrawableHeadNote(this, key)
+                head = new DrawableHeadNote(this, action)
                 {
                     Anchor = Anchor.TopCentre,
                     Origin = Anchor.TopCentre
                 },
-                tail = new DrawableTailNote(this, key)
+                tail = new DrawableTailNote(this, action)
                 {
                     Anchor = Anchor.BottomCentre,
                     Origin = Anchor.TopCentre
@@ -106,16 +104,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
         {
         }
 
-        protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
+        public bool OnPressed(ManiaAction action)
         {
-            // Make sure the keypress happened within the body of the hold note
+            // Make sure the action happened within the body of the hold note
             if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime)
                 return false;
 
-            if (args.Key != Key)
-                return false;
-
-            if (args.Repeat)
+            if (action != Action)
                 return false;
 
             // The user has pressed during the body of the hold note, after the head note and its hit windows have passed
@@ -126,13 +121,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
             return true;
         }
 
-        protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
+        public bool OnReleased(ManiaAction action)
         {
             // Make sure that the user started holding the key during the hold note
             if (!holdStartTime.HasValue)
                 return false;
 
-            if (args.Key != Key)
+            if (action != Action)
                 return false;
 
             holdStartTime = null;
@@ -151,8 +146,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
         {
             private readonly DrawableHoldNote holdNote;
 
-            public DrawableHeadNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
-                : base(holdNote.HitObject.Head, key)
+            public DrawableHeadNote(DrawableHoldNote holdNote, ManiaAction action)
+                : base(holdNote.HitObject.Head, action)
             {
                 this.holdNote = holdNote;
 
@@ -160,9 +155,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
                 Y = 0;
             }
 
-            protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
+            public override bool OnPressed(ManiaAction action)
             {
-                if (!base.OnKeyDown(state, args))
+                if (!base.OnPressed(action))
                     return false;
 
                 // We only want to trigger a holding state from the head if the head has received a judgement
@@ -188,8 +183,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
         {
             private readonly DrawableHoldNote holdNote;
 
-            public DrawableTailNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
-                : base(holdNote.HitObject.Tail, key)
+            public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action)
+                : base(holdNote.HitObject.Tail, action)
             {
                 this.holdNote = holdNote;
 
@@ -210,7 +205,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
                 tailJudgement.HasBroken = holdNote.hasBroken;
             }
 
-            protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
+            public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down
+
+            public override bool OnReleased(ManiaAction action)
             {
                 // Make sure that the user started holding the key during the hold note
                 if (!holdNote.holdStartTime.HasValue)
@@ -219,7 +216,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
                 if (Judgement.Result != HitResult.None)
                     return false;
 
-                if (args.Key != Key)
+                if (action != Action)
                     return false;
 
                 UpdateJudgement(true);
@@ -227,12 +224,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
                 // Handled by the hold note, which will set holding = false
                 return false;
             }
-
-            protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
-            {
-                // Tail doesn't handle key down
-                return false;
-            }
         }
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 10dc607ec3..bfef05ea07 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -2,8 +2,6 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using OpenTK.Graphics;
-using OpenTK.Input;
-using osu.Framework.Configuration;
 using osu.Game.Rulesets.Mania.Judgements;
 using osu.Game.Rulesets.Objects.Drawables;
 
@@ -15,17 +13,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
         /// <summary>
         /// The key that will trigger input for this hit object.
         /// </summary>
-        protected Bindable<Key> Key { get; private set; } = new Bindable<Key>();
+        protected ManiaAction Action { get; }
 
         public new TObject HitObject;
 
-        protected DrawableManiaHitObject(TObject hitObject, Bindable<Key> key = null)
+        protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null)
             : base(hitObject)
         {
             HitObject = hitObject;
 
-            if (key != null)
-                Key.BindTo(key);
+            if (action != null)
+                Action = action.Value;
         }
 
         public override Color4 AccentColour
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 9322fed3eb..c201ab7bd0 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -3,10 +3,8 @@
 
 using System;
 using OpenTK.Graphics;
-using OpenTK.Input;
-using osu.Framework.Configuration;
 using osu.Framework.Graphics;
-using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.Mania.Judgements;
 using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
 using osu.Game.Rulesets.Objects.Drawables;
@@ -16,12 +14,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
     /// <summary>
     /// Visualises a <see cref="Note"/> hit object.
     /// </summary>
-    public class DrawableNote : DrawableManiaHitObject<Note>
+    public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction>
     {
         private readonly NotePiece headPiece;
 
-        public DrawableNote(Note hitObject, Bindable<Key> key = null)
-            : base(hitObject, key)
+        public DrawableNote(Note hitObject, ManiaAction action)
+            : base(hitObject, action)
         {
             RelativeSizeAxes = Axes.X;
             Height = 100;
@@ -81,18 +79,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
             }
         }
 
-        protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
+        public virtual bool OnPressed(ManiaAction action)
         {
-            if (Judgement.Result != HitResult.None)
-                return false;
-
-            if (args.Key != Key)
-                return false;
-
-            if (args.Repeat)
+            if (action != Action)
                 return false;
 
             return UpdateJudgement(true);
         }
+
+        public virtual bool OnReleased(ManiaAction action) => false;
     }
 }
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 9fbc9ba5e7..a8f5b4919d 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -3,17 +3,15 @@
 
 using OpenTK;
 using OpenTK.Graphics;
-using OpenTK.Input;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Colour;
-using osu.Framework.Input;
 using osu.Game.Graphics;
 using osu.Game.Rulesets.Objects.Drawables;
 using System;
-using osu.Framework.Configuration;
+using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.UI;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.Judgements;
@@ -32,10 +30,7 @@ namespace osu.Game.Rulesets.Mania.UI
         private const float column_width = 45;
         private const float special_column_width = 70;
 
-        /// <summary>
-        /// The key that will trigger input actions for this column and hit objects contained inside it.
-        /// </summary>
-        public Bindable<Key> Key = new Bindable<Key>();
+        public ManiaAction Action;
 
         private readonly Box background;
         private readonly Container hitTargetBar;
@@ -101,8 +96,8 @@ namespace osu.Game.Rulesets.Mania.UI
                         // For column lighting, we need to capture input events before the notes
                         new InputTarget
                         {
-                            KeyDown = onKeyDown,
-                            KeyUp = onKeyUp
+                            Pressed = onPressed,
+                            Released = onReleased
                         }
                     }
                 },
@@ -199,12 +194,9 @@ namespace osu.Game.Rulesets.Mania.UI
             HitObjects.Add(hitObject);
         }
 
-        private bool onKeyDown(InputState state, KeyDownEventArgs args)
+        private bool onPressed(ManiaAction action)
         {
-            if (args.Repeat)
-                return false;
-
-            if (args.Key == Key)
+            if (action == Action)
             {
                 background.FadeTo(background.Alpha + 0.2f, 50, Easing.OutQuint);
                 keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint);
@@ -213,9 +205,9 @@ namespace osu.Game.Rulesets.Mania.UI
             return false;
         }
 
-        private bool onKeyUp(InputState state, KeyUpEventArgs args)
+        private bool onReleased(ManiaAction action)
         {
-            if (args.Key == Key)
+            if (action == Action)
             {
                 background.FadeTo(0.2f, 800, Easing.OutQuart);
                 keyIcon.ScaleTo(1f, 400, Easing.OutQuart);
@@ -227,10 +219,10 @@ namespace osu.Game.Rulesets.Mania.UI
         /// <summary>
         /// This is a simple container which delegates various input events that have to be captured before the notes.
         /// </summary>
-        private class InputTarget : Container
+        private class InputTarget : Container, IKeyBindingHandler<ManiaAction>
         {
-            public Func<InputState, KeyDownEventArgs, bool> KeyDown;
-            public Func<InputState, KeyUpEventArgs, bool> KeyUp;
+            public Func<ManiaAction, bool> Pressed;
+            public Func<ManiaAction, bool> Released;
 
             public InputTarget()
             {
@@ -239,8 +231,8 @@ namespace osu.Game.Rulesets.Mania.UI
                 Alpha = 0;
             }
 
-            protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => KeyDown?.Invoke(state, args) ?? false;
-            protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => KeyUp?.Invoke(state, args) ?? false;
+            public bool OnPressed(ManiaAction action) => Pressed?.Invoke(action) ?? false;
+            public bool OnReleased(ManiaAction action) => Released?.Invoke(action) ?? false;
         }
     }
 }
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 0c9351cad2..be14585354 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -122,12 +122,16 @@ namespace osu.Game.Rulesets.Mania.UI
                 }
             };
 
+            var currentAction = ManiaAction.Key1;
             for (int i = 0; i < columnCount; i++)
             {
                 var c = new Column();
                 c.Reversed.BindTo(Reversed);
                 c.VisibleTimeRange.BindTo(VisibleTimeRange);
 
+                c.IsSpecial = isSpecialColumn(i);
+                c.Action = c.IsSpecial ? ManiaAction.Special : currentAction++;
+
                 columns.Add(c);
                 AddNested(c);
             }
@@ -145,15 +149,11 @@ namespace osu.Game.Rulesets.Mania.UI
             specialColumnColour = colours.BlueDark;
 
             // Set the special column + colour + key
-            for (int i = 0; i < columnCount; i++)
+            foreach (var column in Columns)
             {
-                Column column = Columns.ElementAt(i);
-                column.IsSpecial = isSpecialColumn(i);
-
                 if (!column.IsSpecial)
                     continue;
 
-                column.Key.Value = Key.Space;
                 column.AccentColour = specialColumnColour;
             }
 
@@ -167,21 +167,6 @@ namespace osu.Game.Rulesets.Mania.UI
                 nonSpecialColumns[i].AccentColour = colour;
                 nonSpecialColumns[nonSpecialColumns.Count - 1 - i].AccentColour = colour;
             }
-
-            // We'll set the keys for non-special columns in another separate loop because it's not mirrored like the above colours
-            // Todo: This needs to go when we get to bindings and use Button1, ..., ButtonN instead
-            for (int i = 0; i < nonSpecialColumns.Count; i++)
-            {
-                Column column = nonSpecialColumns[i];
-
-                int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i;
-                if (keyOffset >= 0 && keyOffset < default_keys.Length)
-                    column.Key.Value = default_keys[keyOffset];
-                else
-                    // There is no default key defined for this column. Let's set this to Unknown for now
-                    // however note that this will be gone after bindings are in place
-                    column.Key.Value = Key.Unknown;
-            }
         }
 
         /// <summary>
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
index 0e750a348e..5a3da6d074 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
@@ -5,9 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using OpenTK;
-using OpenTK.Input;
 using osu.Framework.Allocation;
-using osu.Framework.Configuration;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Input;
@@ -85,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.UI
 
         public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
 
-        public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo);
+        public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, availableColumns);
 
         protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter()
         {
@@ -109,15 +107,15 @@ namespace osu.Game.Rulesets.Mania.UI
 
         protected override DrawableHitObject<ManiaHitObject, ManiaJudgement> GetVisualRepresentation(ManiaHitObject h)
         {
-            Bindable<Key> key = Playfield.Columns.ElementAt(h.Column).Key;
+            ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action;
 
             var holdNote = h as HoldNote;
             if (holdNote != null)
-                return new DrawableHoldNote(holdNote, key);
+                return new DrawableHoldNote(holdNote, action);
 
             var note = h as Note;
             if (note != null)
-                return new DrawableNote(note, key);
+                return new DrawableNote(note, action);
 
             return null;
         }
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs
index 3b0cfc1ef1..432c6d391c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs
@@ -7,8 +7,13 @@ using osu.Game.Rulesets.Osu.Replays;
 using osu.Game.Rulesets.Mods;
 using osu.Game.Rulesets.Osu.Objects;
 using System;
+using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using OpenTK;
 
 namespace osu.Game.Rulesets.Osu.Mods
 {
@@ -28,10 +33,23 @@ namespace osu.Game.Rulesets.Osu.Mods
         public override double ScoreMultiplier => 1.06;
     }
 
-    public class OsuModHardRock : ModHardRock
+    public class OsuModHardRock : ModHardRock, IApplicableMod<OsuHitObject>
     {
         public override double ScoreMultiplier => 1.06;
         public override bool Ranked => true;
+
+        public void ApplyToRulesetContainer(RulesetContainer<OsuHitObject> rulesetContainer)
+        {
+            rulesetContainer.Objects.OfType<OsuHitObject>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Y));
+            rulesetContainer.Objects.OfType<Slider>().ForEach(s =>
+            {
+                var newControlPoints = new List<Vector2>();
+                s.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, OsuPlayfield.BASE_SIZE.Y - c.Y)));
+
+                s.ControlPoints = newControlPoints;
+                s.Curve?.Calculate(); // Recalculate the slider curve
+            });
+        }
     }
 
     public class OsuModSuddenDeath : ModSuddenDeath
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index ecaf0db096..eb456d7a63 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -13,7 +13,6 @@ using osu.Framework.Platform;
 using osu.Game.IPC;
 using osu.Framework.Allocation;
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
 
 namespace osu.Game.Tests.Beatmaps.IO
 {
@@ -98,16 +97,14 @@ namespace osu.Game.Tests.Beatmaps.IO
 
         private OsuGameBase loadOsu(GameHost host)
         {
+            host.Storage.DeleteDatabase(@"client");
+
             var osu = new OsuGameBase();
             Task.Run(() => host.Run(osu));
 
             while (!osu.IsLoaded)
                 Thread.Sleep(1);
 
-            //reset beatmap database (sqlite and storage backing)
-            osu.Dependencies.Get<RulesetStore>().Reset();
-            osu.Dependencies.Get<BeatmapManager>().Reset();
-
             return osu;
         }
 
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
index 56302acafe..c5ba1683dd 100644
--- a/osu.Game/Input/KeyBindingStore.cs
+++ b/osu.Game/Input/KeyBindingStore.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Input
             {
                 var ruleset = info.CreateInstance();
                 foreach (var variant in ruleset.AvailableVariants)
-                    insertDefaults(ruleset.GetDefaultKeyBindings(), info.ID, variant);
+                    insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
             }
         }
 
diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
index 7dd9919e5d..8ebd4ac545 100644
--- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs
@@ -3,21 +3,29 @@
 
 using osu.Framework.Input.Bindings;
 using osu.Game.Graphics;
+using osu.Game.Overlays.Settings;
 
 namespace osu.Game.Overlays.KeyBinding
 {
-    public class GlobalKeyBindingsSection : KeyBindingsSection
+    public class GlobalKeyBindingsSection : SettingsSection
     {
-        private readonly string name;
+        public override FontAwesome Icon => FontAwesome.fa_osu_hot;
+        public override string Header => "Global";
 
-        public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail;
-        public override string Header => name;
-
-        public GlobalKeyBindingsSection(KeyBindingInputManager manager, string name)
+        public GlobalKeyBindingsSection(KeyBindingInputManager manager)
         {
-            this.name = name;
+            Add(new DefaultBindingsSubsection(manager));
+        }
 
-            Defaults = manager.DefaultKeyBindings;
+        private class DefaultBindingsSubsection : KeyBindingsSubsection
+        {
+            protected override string Header => string.Empty;
+
+            public DefaultBindingsSubsection(KeyBindingInputManager manager)
+                : base(null)
+            {
+                Defaults = manager.DefaultKeyBindings;
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index 6c697c8660..c182382d70 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -1,7 +1,6 @@
 // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using System;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
@@ -22,7 +21,7 @@ namespace osu.Game.Overlays.KeyBinding
 {
     internal class KeyBindingRow : Container, IFilterable
     {
-        private readonly Enum action;
+        private readonly object action;
         private readonly IEnumerable<Framework.Input.Bindings.KeyBinding> bindings;
 
         private const float transition_time = 150;
@@ -50,7 +49,7 @@ namespace osu.Game.Overlays.KeyBinding
 
         public string[] FilterTerms => new[] { text.Text }.Concat(bindings.Select(b => b.KeyCombination.ReadableString())).ToArray();
 
-        public KeyBindingRow(Enum action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
+        public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
         {
             this.action = action;
             this.bindings = bindings;
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
similarity index 57%
rename from osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs
rename to osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
index 44c28ee9c8..38a5b1a171 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs
@@ -1,7 +1,6 @@
 // Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using System;
 using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Allocation;
@@ -12,38 +11,34 @@ using OpenTK;
 
 namespace osu.Game.Overlays.KeyBinding
 {
-    public abstract class KeyBindingsSection : SettingsSection
+    public abstract class KeyBindingsSubsection : SettingsSubsection
     {
         protected IEnumerable<Framework.Input.Bindings.KeyBinding> Defaults;
 
         protected RulesetInfo Ruleset;
 
-        protected KeyBindingsSection()
+        private readonly int? variant;
+
+        protected KeyBindingsSubsection(int? variant)
         {
+            this.variant = variant;
+
             FlowContent.Spacing = new Vector2(0, 1);
         }
 
         [BackgroundDependencyLoader]
         private void load(KeyBindingStore store)
         {
-            var enumType = Defaults?.FirstOrDefault()?.Action?.GetType();
-
-            if (enumType == null) return;
-
-            // for now let's just assume a variant of zero.
-            // this will need to be implemented in a better way in the future.
-            int? variant = null;
-            if (Ruleset != null)
-                variant = 0;
-
             var bindings = store.Query(Ruleset?.ID, variant);
 
-            foreach (Enum v in Enum.GetValues(enumType))
+            foreach (var defaultBinding in Defaults)
+            {
                 // one row per valid action.
-                Add(new KeyBindingRow(v, bindings.Where(b => b.Action.Equals((int)(object)v)))
+                Add(new KeyBindingRow(defaultBinding.Action, bindings.Where(b => b.Action.Equals((int)defaultBinding.Action)))
                 {
                     AllowMainMouseButtons = Ruleset != null
                 });
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
index 20941115e3..885e149cb2 100644
--- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
+++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs
@@ -2,20 +2,26 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using osu.Game.Graphics;
+using osu.Game.Overlays.Settings;
 using osu.Game.Rulesets;
 
 namespace osu.Game.Overlays.KeyBinding
 {
-    public class RulesetBindingsSection : KeyBindingsSection
+    public class RulesetBindingsSection : SettingsSection
     {
-        public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail;
-        public override string Header => Ruleset.Name;
+        public override FontAwesome Icon => FontAwesome.fa_osu_hot;
+        public override string Header => ruleset.Name;
+
+        private readonly RulesetInfo ruleset;
 
         public RulesetBindingsSection(RulesetInfo ruleset)
         {
-            Ruleset = ruleset;
+            this.ruleset = ruleset;
 
-            Defaults = ruleset.CreateInstance().GetDefaultKeyBindings();
+            var r = ruleset.CreateInstance();
+
+            foreach (var variant in r.AvailableVariants)
+                Add(new VariantBindingsSubsection(ruleset, variant));
         }
     }
 }
\ No newline at end of file
diff --git a/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs
new file mode 100644
index 0000000000..dca5f53b4a
--- /dev/null
+++ b/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets;
+
+namespace osu.Game.Overlays.KeyBinding
+{
+    public class VariantBindingsSubsection : KeyBindingsSubsection
+    {
+        protected override string Header => variantName;
+        private readonly string variantName;
+
+        public VariantBindingsSubsection(RulesetInfo ruleset, int variant)
+            : base(variant)
+        {
+            Ruleset = ruleset;
+
+            var rulesetInstance = ruleset.CreateInstance();
+
+            variantName = rulesetInstance.GetVariantName(variant);
+            Defaults = rulesetInstance.GetDefaultKeyBindings(variant);
+        }
+    }
+}
\ No newline at end of file
diff --git a/osu.Game/Overlays/KeyBindingOverlay.cs b/osu.Game/Overlays/KeyBindingOverlay.cs
index 827d361099..72c653030c 100644
--- a/osu.Game/Overlays/KeyBindingOverlay.cs
+++ b/osu.Game/Overlays/KeyBindingOverlay.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Overlays
         [BackgroundDependencyLoader(permitNulls: true)]
         private void load(RulesetStore rulesets, GlobalKeyBindingInputManager global)
         {
-            AddSection(new GlobalKeyBindingsSection(global, "Global"));
+            AddSection(new GlobalKeyBindingsSection(global));
 
             foreach (var ruleset in rulesets.AllRulesets)
                 AddSection(new RulesetBindingsSection(ruleset));
diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs
index ac6d2fa239..0fbb5b92f7 100644
--- a/osu.Game/Overlays/Settings/SettingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs
@@ -7,14 +7,15 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using System.Collections.Generic;
 using System.Linq;
+using osu.Framework.Allocation;
 
 namespace osu.Game.Overlays.Settings
 {
     public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren
     {
-        protected override Container<Drawable> Content => content;
+        protected override Container<Drawable> Content => FlowContent;
 
-        private readonly Container<Drawable> content;
+        protected readonly FillFlowContainer FlowContent;
 
         protected abstract string Header { get; }
 
@@ -33,6 +34,19 @@ namespace osu.Game.Overlays.Settings
             RelativeSizeAxes = Axes.X;
             AutoSizeAxes = Axes.Y;
             Direction = FillDirection.Vertical;
+
+            FlowContent = new FillFlowContainer
+            {
+                Direction = FillDirection.Vertical,
+                Spacing = new Vector2(0, 5),
+                RelativeSizeAxes = Axes.X,
+                AutoSizeAxes = Axes.Y,
+            };
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
             AddRangeInternal(new Drawable[]
             {
                 new OsuSpriteText
@@ -41,13 +55,7 @@ namespace osu.Game.Overlays.Settings
                     Margin = new MarginPadding { Bottom = 10 },
                     Font = @"Exo2.0-Black",
                 },
-                content = new FillFlowContainer
-                {
-                    Direction = FillDirection.Vertical,
-                    Spacing = new Vector2(0, 5),
-                    RelativeSizeAxes = Axes.X,
-                    AutoSizeAxes = Axes.Y,
-                },
+                FlowContent
             });
         }
     }
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 32d958b462..c54aeb7852 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -63,5 +63,12 @@ namespace osu.Game.Rulesets
         /// <param name="variant">A variant.</param>
         /// <returns>A list of valid <see cref="KeyBinding"/>s.</returns>
         public virtual IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new KeyBinding[] { };
+
+        /// <summary>
+        /// Gets the name for a key binding variant. This is used for display in the settings overlay.
+        /// </summary>
+        /// <param name="variant">The variant.</param>
+        /// <returns>A descriptive name of the variant.</returns>
+        public virtual string GetVariantName(int variant) => string.Empty;
     }
 }
diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs
index c6421522b4..c06d31e38f 100644
--- a/osu.Game/Rulesets/Scoring/ScoreStore.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs
@@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Scoring
                 frames.Add(new ReplayFrame(
                     lastTime,
                     float.Parse(split[1]),
-                    384 - float.Parse(split[2]),
+                    float.Parse(split[2]),
                     (ReplayButtonState)int.Parse(split[3])
                 ));
             }
diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs
index 2f1cb915f3..84cadeb2a1 100644
--- a/osu.Game/Rulesets/UI/RulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/RulesetContainer.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI
         /// <summary>
         /// The key conversion input manager for this RulesetContainer.
         /// </summary>
-        public readonly PassThroughInputManager KeyBindingInputManager;
+        public PassThroughInputManager KeyBindingInputManager;
 
         /// <summary>
         /// Whether we are currently providing the local user a gameplay cursor.
@@ -76,6 +76,11 @@ namespace osu.Game.Rulesets.UI
         internal RulesetContainer(Ruleset ruleset)
         {
             Ruleset = ruleset;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
+        {
             KeyBindingInputManager = CreateInputManager();
             KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
         }
@@ -105,7 +110,7 @@ namespace osu.Game.Rulesets.UI
         /// Sets a replay to be used, overriding local input.
         /// </summary>
         /// <param name="replay">The replay, null for local input.</param>
-        public void SetReplay(Replay replay)
+        public virtual void SetReplay(Replay replay)
         {
             Replay = replay;
             InputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null;
@@ -244,7 +249,7 @@ namespace osu.Game.Rulesets.UI
         public Playfield<TObject, TJudgement> Playfield { get; private set; }
 
         protected override Container<Drawable> Content => content;
-        private readonly Container content;
+        private Container content;
 
         private readonly List<DrawableHitObject<TObject, TJudgement>> drawableObjects = new List<DrawableHitObject<TObject, TJudgement>>();
 
@@ -256,6 +261,11 @@ namespace osu.Game.Rulesets.UI
         /// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
         protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
             : base(ruleset, beatmap, isForCurrentRuleset)
+        {
+        }
+
+        [BackgroundDependencyLoader]
+        private void load()
         {
             InputManager.Add(content = new Container
             {
@@ -264,14 +274,14 @@ namespace osu.Game.Rulesets.UI
             });
 
             AddInternal(InputManager);
-        }
-
-        [BackgroundDependencyLoader]
-        private void load()
-        {
             KeyBindingInputManager.Add(Playfield = CreatePlayfield());
 
             loadObjects();
+        }
+
+        public override void SetReplay(Replay replay)
+        {
+            base.SetReplay(replay);
 
             if (InputManager?.ReplayInputHandler != null)
                 InputManager.ReplayInputHandler.ToScreenSpace = Playfield.ScaledContent.ToScreenSpace;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 11b99c1796..32a8df6357 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -105,9 +105,10 @@
     <Compile Include="Overlays\Chat\ChatTabControl.cs" />
     <Compile Include="Overlays\KeyBinding\GlobalKeyBindingsSection.cs" />
     <Compile Include="Overlays\KeyBinding\KeyBindingRow.cs" />
-    <Compile Include="Overlays\KeyBinding\KeyBindingsSection.cs" />
+    <Compile Include="Overlays\KeyBinding\KeyBindingsSubsection.cs" />
     <Compile Include="Overlays\KeyBindingOverlay.cs" />
     <Compile Include="Overlays\KeyBinding\RulesetBindingsSection.cs" />
+    <Compile Include="Overlays\KeyBinding\VariantBindingsSubsection.cs" />
     <Compile Include="Overlays\MainSettings.cs" />
     <Compile Include="Overlays\Music\CollectionsDropdown.cs" />
     <Compile Include="Overlays\Music\FilterControl.cs" />