diff --git a/appveyor.yml b/appveyor.yml
index 15484e4c68..69bc762f4c 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -11,7 +11,7 @@ install:
   - cmd: git submodule update --init --recursive --depth=5
   - cmd: choco install resharper-clt -y
   - cmd: choco install nvika -y
-  - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.4/CodeFileSanity.exe
+  - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.5/CodeFileSanity.exe
 before_build:
   - cmd: CodeFileSanity.exe
   - cmd: nuget restore -verbosity quiet
diff --git a/osu-framework b/osu-framework
index 0773d895d9..8c4f232694 160000
--- a/osu-framework
+++ b/osu-framework
@@ -1 +1 @@
-Subproject commit 0773d895d9aa0729995cd4a23efc28238e35ceed
+Subproject commit 8c4f23269447d9ce21a5dbd3a0fd4f6caae9ab38
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index bd0cc209b6..d0d623178e 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -14,7 +14,7 @@ using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Rulesets.Catch.Tests
 {
-    public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
+    public class CatchBeatmapConversionTest : BeatmapConversionTest<TestCatchRuleset, ConvertValue>
     {
         protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
 
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Tests
             }
         }
 
-        protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new CatchBeatmapConverter();
+        protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
     }
 
     public struct ConvertValue : IEquatable<ConvertValue>
@@ -64,4 +64,8 @@ namespace osu.Game.Rulesets.Catch.Tests
             => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
                && Precision.AlmostEquals(Position, other.Position, conversion_lenience);
     }
+
+    public class TestCatchRuleset : CatchRuleset
+    {
+    }
 }
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
index bce20520d3..097750d7e0 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override Beatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
         {
             var beatmap = new Beatmap
             {
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
index d13a6bb860..b5cf0e3d1d 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override Beatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
         {
             var beatmap = new Beatmap
             {
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
index 2b58fcc93c..8a90b48180 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override Beatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
         {
             var beatmap = new Beatmap
             {
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs
index e7f936ca2a..896582bf0a 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests
         {
         }
 
-        protected override Beatmap CreateBeatmap(Ruleset ruleset)
+        protected override IBeatmap CreateBeatmap(Ruleset ruleset)
         {
             var beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo } };
 
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
new file mode 100644
index 0000000000..5b4af6ea8a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Beatmaps
+{
+    public class CatchBeatmap : Beatmap<CatchHitObject>
+    {
+        public override IEnumerable<BeatmapStatistic> GetStatistics()
+        {
+            int fruits = HitObjects.Count(s => s is Fruit);
+            int juiceStreams = HitObjects.Count(s => s is JuiceStream);
+            int bananaShowers = HitObjects.Count(s => s is BananaShower);
+
+            return new[]
+            {
+                new BeatmapStatistic
+                {
+                    Name = @"Fruit Count",
+                    Content = fruits.ToString(),
+                    Icon = FontAwesome.fa_circle_o
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Juice Stream Count",
+                    Content = juiceStreams.ToString(),
+                    Icon = FontAwesome.fa_circle
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Banana Shower Count",
+                    Content = bananaShowers.ToString(),
+                    Icon = FontAwesome.fa_circle
+                }
+            };
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 34e5f425fd..f40ef67dfb 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -13,9 +13,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
 {
     public class CatchBeatmapConverter : BeatmapConverter<CatchHitObject>
     {
+        public CatchBeatmapConverter(IBeatmap beatmap)
+            : base(beatmap)
+        {
+        }
+
         protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
 
-        protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, Beatmap beatmap)
+        protected override IEnumerable<CatchHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
         {
             var curveData = obj as IHasCurve;
             var positionData = obj as IHasXPosition;
@@ -64,5 +69,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
                 X = positionData.X / CatchPlayfield.BASE_WIDTH
             };
         }
+
+        protected override Beatmap<CatchHitObject> CreateBeatmap() => new CatchBeatmap();
     }
 }
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index dfd10e0df7..e16f5fcb60 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -12,16 +12,21 @@ using OpenTK;
 
 namespace osu.Game.Rulesets.Catch.Beatmaps
 {
-    public class CatchBeatmapProcessor : BeatmapProcessor<CatchHitObject>
+    public class CatchBeatmapProcessor : BeatmapProcessor
     {
-        public override void PostProcess(Beatmap<CatchHitObject> beatmap)
+        public CatchBeatmapProcessor(IBeatmap beatmap)
+            : base(beatmap)
         {
-            initialiseHyperDash(beatmap.HitObjects);
+        }
 
-            base.PostProcess(beatmap);
+        public override void PostProcess()
+        {
+            initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
+
+            base.PostProcess();
 
             int index = 0;
-            foreach (var obj in beatmap.HitObjects)
+            foreach (var obj in Beatmap.HitObjects.OfType<CatchHitObject>())
                 obj.IndexInBeatmap = index++;
         }
 
diff --git a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs
index 876b394da0..f47d09fe20 100644
--- a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs
@@ -2,20 +2,16 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Catch.Beatmaps;
-using osu.Game.Rulesets.Catch.Objects;
 using System.Collections.Generic;
 
 namespace osu.Game.Rulesets.Catch
 {
-    public class CatchDifficultyCalculator : DifficultyCalculator<CatchHitObject>
+    public class CatchDifficultyCalculator : DifficultyCalculator
     {
-        public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap)
+        public CatchDifficultyCalculator(IBeatmap beatmap) : base(beatmap)
         {
         }
 
         public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => 0;
-
-        protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter();
     }
 }
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index cfe0fc5cec..15e51fa126 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -13,12 +13,15 @@ using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.Catch.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Catch.Beatmaps;
 
 namespace osu.Game.Rulesets.Catch
 {
     public class CatchRuleset : Ruleset
     {
-        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new CatchRulesetContainer(this, beatmap, isForCurrentRuleset);
+        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new CatchRulesetContainer(this, beatmap);
+        public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
+        public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
 
         public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
         {
@@ -138,7 +141,7 @@ namespace osu.Game.Rulesets.Catch
 
         public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
 
-        public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap);
+        public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap);
 
         public override int? LegacyID => 2;
 
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index df7578799f..8e19c0614a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -6,10 +6,11 @@ using osu.Game.Rulesets.Catch.Objects;
 using osu.Game.Rulesets.Catch.UI;
 using osu.Game.Rulesets.Mods;
 using System;
+using osu.Game.Rulesets.Objects;
 
 namespace osu.Game.Rulesets.Catch.Mods
 {
-    public class CatchModHardRock : ModHardRock, IApplicableToHitObject<CatchHitObject>
+    public class CatchModHardRock : ModHardRock, IApplicableToHitObject
     {
         public override double ScoreMultiplier => 1.12;
         public override bool Ranked => true;
@@ -17,9 +18,11 @@ namespace osu.Game.Rulesets.Catch.Mods
         private float lastStartX;
         private int lastStartTime;
 
-        public void ApplyToHitObject(CatchHitObject hitObject)
+        public void ApplyToHitObject(HitObject hitObject)
         {
-            float position = hitObject.X;
+            var catchObject = (CatchHitObject)hitObject;
+
+            float position = catchObject.X;
             int startTime = (int)hitObject.StartTime;
 
             if (lastStartX == 0)
@@ -60,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Mods
                         position += rand;
                 }
 
-                hitObject.X = position;
+                catchObject.X = position;
 
                 return;
             }
@@ -79,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Mods
                 }
             }
 
-            hitObject.X = position;
+            catchObject.X = position;
 
             lastStartX = position;
             lastStartTime = startTime;
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index d63d1bd331..d5c5eb844a 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Replays
             Dashing = dashing;
         }
 
-        public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
+        public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
         {
             Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH;
             Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1;
diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
index 022a8a8b43..070dc19a6f 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
@@ -4,7 +4,6 @@
 using osu.Framework.Input;
 using osu.Game.Beatmaps;
 using osu.Game.Input.Handlers;
-using osu.Game.Rulesets.Catch.Beatmaps;
 using osu.Game.Rulesets.Catch.Objects;
 using osu.Game.Rulesets.Catch.Objects.Drawable;
 using osu.Game.Rulesets.Catch.Replays;
@@ -20,8 +19,8 @@ namespace osu.Game.Rulesets.Catch.UI
 {
     public class CatchRulesetContainer : ScrollingRulesetContainer<CatchPlayfield, CatchHitObject>
     {
-        public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
 
@@ -29,10 +28,6 @@ namespace osu.Game.Rulesets.Catch.UI
 
         protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
 
-        protected override BeatmapProcessor<CatchHitObject> CreateBeatmapProcessor() => new CatchBeatmapProcessor();
-
-        protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter() => new CatchBeatmapConverter();
-
         protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, GetVisualRepresentation);
 
         public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
index 81c537e53c..f1ee874b88 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs
@@ -14,17 +14,14 @@ using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Rulesets.Mania.Tests
 {
-    public class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
+    public class ManiaBeatmapConversionTest : BeatmapConversionTest<TestManiaRuleset, ConvertValue>
     {
         protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
 
-        private bool isForCurrentRuleset;
-
         [NonParallelizable]
-        [TestCase("basic", false)]
-        public void Test(string name, bool isForCurrentRuleset)
+        [TestCase("basic")]
+        public new void Test(string name)
         {
-            this.isForCurrentRuleset = isForCurrentRuleset;
             base.Test(name);
         }
 
@@ -38,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests
             };
         }
 
-        protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new ManiaBeatmapConverter(isForCurrentRuleset, beatmap);
+        protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
     }
 
     public struct ConvertValue : IEquatable<ConvertValue>
@@ -57,4 +54,8 @@ namespace osu.Game.Rulesets.Mania.Tests
                && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
                && Column == other.Column;
     }
+
+    public class TestManiaRuleset : ManiaRuleset
+    {
+    }
 }
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index 6af3719f83..ad5f8e447d 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using osu.Game.Beatmaps;
+using osu.Game.Graphics;
 using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mania.UI;
 
@@ -29,5 +30,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
         {
             Stages.Add(defaultStage);
         }
+
+        public override IEnumerable<BeatmapStatistic> GetStatistics()
+        {
+            int notes = HitObjects.Count(s => s is Note);
+            int holdnotes = HitObjects.Count(s => s is HoldNote);
+
+            return new[]
+            {
+                new BeatmapStatistic
+                {
+                    Name = @"Note Count",
+                    Content = notes.ToString(),
+                    Icon = FontAwesome.fa_circle_o
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Hold Note Count",
+                    Content = holdnotes.ToString(),
+                    Icon = FontAwesome.fa_circle
+                },
+            };
+        }
     }
 }
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 60b92cb7b3..c8a7402904 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -33,18 +33,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
 
         private ManiaBeatmap beatmap;
 
-        public ManiaBeatmapConverter(bool isForCurrentRuleset, Beatmap original)
+        public ManiaBeatmapConverter(IBeatmap beatmap)
+            : base(beatmap)
         {
-            IsForCurrentRuleset = isForCurrentRuleset;
+            IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
 
-            var roundedCircleSize = Math.Round(original.BeatmapInfo.BaseDifficulty.CircleSize);
-            var roundedOverallDifficulty = Math.Round(original.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+            var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+            var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
 
-            if (isForCurrentRuleset)
+            if (IsForCurrentRuleset)
                 TargetColumns = (int)Math.Max(1, roundedCircleSize);
             else
             {
-                float percentSliderOrSpinner = (float)original.HitObjects.Count(h => h is IHasEndTime) / original.HitObjects.Count;
+                float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count();
                 if (percentSliderOrSpinner < 0.2)
                     TargetColumns = 7;
                 else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
@@ -56,8 +57,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
             }
         }
 
-        protected override Beatmap<ManiaHitObject> ConvertBeatmap(Beatmap original)
+        protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
         {
+
             BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
 
             int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate);
@@ -68,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
 
         protected override Beatmap<ManiaHitObject> CreateBeatmap() => beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
 
-        protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, Beatmap beatmap)
+        protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
         {
             var maniaOriginal = original as ManiaHitObject;
             if (maniaOriginal != null)
@@ -112,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
         /// <param name="original">The original hit object.</param>
         /// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
         /// <returns>The hit objects generated.</returns>
-        private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, Beatmap originalBeatmap)
+        private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
         {
             var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap);
 
@@ -128,7 +130,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
         /// <param name="original">The original hit object.</param>
         /// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
         /// <returns>The hit objects generated.</returns>
-        private IEnumerable<ManiaHitObject> generateConverted(HitObject original, Beatmap originalBeatmap)
+        private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
         {
             var endTimeData = original as IHasEndTime;
             var distanceData = original as IHasDistance;
@@ -165,7 +167,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
         /// </summary>
         private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
         {
-            public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
+            public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
                 : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
             {
             }
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 3b5c028bfd..afa9bdbbd7 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
         private PatternType convertType;
 
-        public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
+        public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
             : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
         {
             convertType = PatternType.None;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index 743e230cb2..3f34afee85 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
     {
         private readonly double endTime;
 
-        public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Beatmap originalBeatmap)
+        public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
             : base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
         {
             var endtimeData = HitObject as IHasEndTime;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
index 652c92dd78..cec3e18ad6 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
 
         private readonly PatternType convertType;
 
-        public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, Beatmap originalBeatmap)
+        public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, IBeatmap originalBeatmap)
             : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
         {
             if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime));
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index 02306846a3..930597c1ad 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
         /// <summary>
         /// The beatmap which <see cref="HitObject"/> is being converted from.
         /// </summary>
-        protected readonly Beatmap OriginalBeatmap;
+        protected readonly IBeatmap OriginalBeatmap;
 
-        protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap)
+        protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
             : base(hitObject, beatmap, previousPattern)
         {
             if (random == null) throw new ArgumentNullException(nameof(random));
@@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
                 drainTime /= 1000;
 
                 BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
-                conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
+                conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15;
                 conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
 
                 return conversionDifficulty.Value;
diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs
index 5eea346836..822ba53eeb 100644
--- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs
@@ -10,7 +10,7 @@ using System.Collections.Generic;
 
 namespace osu.Game.Rulesets.Mania
 {
-    internal class ManiaDifficultyCalculator : DifficultyCalculator<ManiaHitObject>
+    internal class ManiaDifficultyCalculator : DifficultyCalculator
     {
         private const double star_scaling_factor = 0.018;
 
@@ -31,12 +31,12 @@ namespace osu.Game.Rulesets.Mania
         /// </summary>
         private readonly List<ManiaHitObjectDifficulty> difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
 
-        public ManiaDifficultyCalculator(Beatmap beatmap)
+        public ManiaDifficultyCalculator(IBeatmap beatmap)
             : base(beatmap)
         {
         }
 
-        public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods)
+        public ManiaDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
             : base(beatmap, mods)
         {
         }
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
             int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7;
 
             foreach (var hitObject in Beatmap.HitObjects)
-                difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount));
+                difficultyHitObjects.Add(new ManiaHitObjectDifficulty((ManiaHitObject)hitObject, columnCount));
 
             // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
             difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
@@ -140,7 +140,5 @@ namespace osu.Game.Rulesets.Mania
 
             return difficulty;
         }
-
-        protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap);
     }
 }
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 0546cbc174..f1d65f855b 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -15,12 +15,14 @@ using osu.Game.Graphics;
 using osu.Game.Rulesets.Mania.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Mania.Beatmaps;
 
 namespace osu.Game.Rulesets.Mania
 {
     public class ManiaRuleset : Ruleset
     {
-        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaRulesetContainer(this, beatmap, isForCurrentRuleset);
+        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap);
+        public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
 
         public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
         {
@@ -182,7 +184,7 @@ namespace osu.Game.Rulesets.Mania
 
         public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
 
-        public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods);
+        public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods);
 
         public override int? LegacyID => 3;
 
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
index dbd30121a8..e02db68a28 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs
@@ -3,19 +3,18 @@
 
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
 using osu.Game.Rulesets.Mods;
 
 namespace osu.Game.Rulesets.Mania.Mods
 {
-    public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter<ManiaHitObject>
+    public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter
     {
         public override string ShortenedName => Name;
         public abstract int KeyCount { get; }
         public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier
         public override bool Ranked => true;
 
-        public void ApplyToBeatmapConverter(BeatmapConverter<ManiaHitObject> beatmapConverter)
+        public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
         {
             var mbc = (ManiaBeatmapConverter)beatmapConverter;
 
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
index 197b37b3f5..7f3985b26d 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
@@ -11,19 +11,23 @@ using osu.Game.Rulesets.UI;
 
 namespace osu.Game.Rulesets.Mania.Mods
 {
-    public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter<ManiaHitObject>, IApplicableToRulesetContainer<ManiaHitObject>
+    public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer<ManiaHitObject>
     {
         public override string Name => "Dual Stages";
         public override string ShortenedName => "DS";
         public override string Description => @"Double the stages, double the fun!";
         public override double ScoreMultiplier => 0;
 
-        public void ApplyToBeatmapConverter(BeatmapConverter<ManiaHitObject> beatmapConverter)
+        private bool isForCurrentRuleset;
+
+        public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)
         {
             var mbc = (ManiaBeatmapConverter)beatmapConverter;
 
+            isForCurrentRuleset = mbc.IsForCurrentRuleset;
+
             // Although this can work, for now let's not allow keymods for mania-specific beatmaps
-            if (mbc.IsForCurrentRuleset)
+            if (isForCurrentRuleset)
                 return;
 
             mbc.TargetColumns *= 2;
@@ -34,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Mods
             var mrc = (ManiaRulesetContainer)rulesetContainer;
 
             // Although this can work, for now let's not allow keymods for mania-specific beatmaps
-            if (mrc.IsForCurrentRuleset)
+            if (isForCurrentRuleset)
                 return;
 
             var newDefinitions = new List<StageDefinition>();
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
index 8d86325dd9..bc9fd6e06f 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
@@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Mania.Replays
             Actions.AddRange(actions);
         }
 
-        public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
+        public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
         {
             // We don't need to fully convert, just create the converter
-            var converter = new ManiaBeatmapConverter(beatmap.BeatmapInfo.RulesetID == 3, beatmap);
+            var converter = new ManiaBeatmapConverter(beatmap);
 
             // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling
             // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage.
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
index 76afaf270f..7123aab901 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI
 
         public IEnumerable<BarLine> BarLines;
 
-        public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
             // Generate the bar lines
             double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
@@ -85,8 +85,6 @@ namespace osu.Game.Rulesets.Mania.UI
 
         public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
 
-        protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter(IsForCurrentRuleset, WorkingBeatmap.Beatmap);
-
         protected override DrawableHitObject<ManiaHitObject> GetVisualRepresentation(ManiaHitObject h)
         {
             ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action;
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
index 6ac3c016a0..aa7de4ed01 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs
@@ -15,7 +15,7 @@ using OpenTK;
 
 namespace osu.Game.Rulesets.Osu.Tests
 {
-    public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
+    public class OsuBeatmapConversionTest : BeatmapConversionTest<TestOsuRuleset, ConvertValue>
     {
         protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
 
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
             };
         }
 
-        protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new OsuBeatmapConverter();
+        protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
     }
 
     public struct ConvertValue : IEquatable<ConvertValue>
@@ -67,4 +67,8 @@ namespace osu.Game.Rulesets.Osu.Tests
                && Precision.AlmostEquals(EndX, other.EndX, conversion_lenience)
                && Precision.AlmostEquals(EndY, other.EndY, conversion_lenience);
     }
+
+    public class TestOsuRuleset : OsuRuleset
+    {
+    }
 }
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
new file mode 100644
index 0000000000..6d90c2a875
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Beatmaps
+{
+    public class OsuBeatmap : Beatmap<OsuHitObject>
+    {
+        public override IEnumerable<BeatmapStatistic> GetStatistics()
+        {
+            int circles = HitObjects.Count(c => c is HitCircle);
+            int sliders = HitObjects.Count(s => s is Slider);
+            int spinners = HitObjects.Count(s => s is Spinner);
+
+            return new[]
+            {
+                new BeatmapStatistic
+                {
+                    Name = @"Circle Count",
+                    Content = circles.ToString(),
+                    Icon = FontAwesome.fa_circle_o
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Slider Count",
+                    Content = sliders.ToString(),
+                    Icon = FontAwesome.fa_circle
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Spinner Count",
+                    Content = spinners.ToString(),
+                    Icon = FontAwesome.fa_circle
+                }
+            };
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index 1236076f48..5f3a909488 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -14,9 +14,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
 {
     internal class OsuBeatmapConverter : BeatmapConverter<OsuHitObject>
     {
+        public OsuBeatmapConverter(IBeatmap beatmap)
+            : base(beatmap)
+        {
+        }
+
         protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasPosition) };
 
-        protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, Beatmap beatmap)
+        protected override IEnumerable<OsuHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap)
         {
             var curveData = original as IHasCurve;
             var endTimeData = original as IHasEndTime;
@@ -60,5 +65,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
                 };
             }
         }
+
+        protected override Beatmap<OsuHitObject> CreateBeatmap() => new OsuBeatmap();
     }
 }
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
index afa2437bf6..c7c9f4a01a 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs
@@ -8,12 +8,17 @@ using osu.Game.Rulesets.Osu.Objects;
 
 namespace osu.Game.Rulesets.Osu.Beatmaps
 {
-    internal class OsuBeatmapProcessor : BeatmapProcessor<OsuHitObject>
+    internal class OsuBeatmapProcessor : BeatmapProcessor
     {
-        public override void PostProcess(Beatmap<OsuHitObject> beatmap)
+        public OsuBeatmapProcessor(IBeatmap beatmap)
+            : base(beatmap)
         {
-            applyStacking(beatmap);
-            base.PostProcess(beatmap);
+        }
+
+        public override void PostProcess()
+        {
+            applyStacking((Beatmap<OsuHitObject>)Beatmap);
+            base.PostProcess();
         }
 
         private void applyStacking(Beatmap<OsuHitObject> beatmap)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
index 8d4c342740..ea33ec9ae0 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
@@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Edit
 {
     public class OsuEditRulesetContainer : OsuRulesetContainer
     {
-        public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
 
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 7bf0651443..dce1fc2851 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit
         {
         }
 
-        protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap, true);
+        protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap);
 
         protected override IReadOnlyList<ICompositionTool> CompositionTools => new ICompositionTool[]
         {
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index cf71116d47..7a30e6b134 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -5,20 +5,23 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.UI;
 using OpenTK;
 
 namespace osu.Game.Rulesets.Osu.Mods
 {
-    public class OsuModHardRock : ModHardRock, IApplicableToHitObject<OsuHitObject>
+    public class OsuModHardRock : ModHardRock, IApplicableToHitObject
     {
         public override double ScoreMultiplier => 1.06;
         public override bool Ranked => true;
 
-        public void ApplyToHitObject(OsuHitObject hitObject)
+        public void ApplyToHitObject(HitObject hitObject)
         {
-            hitObject.Position = new Vector2(hitObject.Position.X, OsuPlayfield.BASE_SIZE.Y - hitObject.Y);
+            var osuObject = (OsuHitObject)hitObject;
+
+            osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y);
 
             var slider = hitObject as Slider;
             if (slider == null)
diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs
index 926a7975f3..4853cd66cd 100644
--- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs
@@ -5,36 +5,30 @@ using System;
 using System.Collections.Generic;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
 using osu.Game.Rulesets.Osu.OsuDifficulty.Skills;
 
 namespace osu.Game.Rulesets.Osu.OsuDifficulty
 {
-    public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
+    public class OsuDifficultyCalculator : DifficultyCalculator
     {
         private const int section_length = 400;
         private const double difficulty_multiplier = 0.0675;
 
-        public OsuDifficultyCalculator(Beatmap beatmap)
+        public OsuDifficultyCalculator(IBeatmap beatmap)
             : base(beatmap)
         {
         }
 
-        public OsuDifficultyCalculator(Beatmap beatmap, Mod[] mods)
+        public OsuDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
             : base(beatmap, mods)
         {
         }
 
-        protected override void PreprocessHitObjects()
-        {
-            new OsuBeatmapProcessor().PostProcess(Beatmap);
-        }
-
         public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
         {
-            OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate);
+            OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap((List<OsuHitObject>)Beatmap.HitObjects, TimeRate);
             Skill[] skills =
             {
                 new Aim(),
@@ -72,7 +66,5 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty
 
             return starRating;
         }
-
-        protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter(Beatmap beatmap) => new OsuBeatmapConverter();
     }
 }
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index e0ecee97a3..69a54fb533 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.OsuDifficulty;
 using osu.Game.Rulesets.Osu.UI;
 using osu.Game.Rulesets.UI;
 using System.Collections.Generic;
-using System.Linq;
 using osu.Framework.Graphics;
 using osu.Game.Overlays.Settings;
 using osu.Framework.Input.Bindings;
@@ -17,17 +16,18 @@ using osu.Game.Rulesets.Scoring;
 using osu.Game.Rulesets.Osu.Scoring;
 using osu.Game.Rulesets.Osu.Edit;
 using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Osu.Replays;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Osu.Beatmaps;
 
 namespace osu.Game.Rulesets.Osu
 {
     public class OsuRuleset : Ruleset
     {
-        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuRulesetContainer(this, beatmap, isForCurrentRuleset);
+        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new OsuRulesetContainer(this, beatmap);
+        public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
+        public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
 
         public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
         {
@@ -37,36 +37,6 @@ namespace osu.Game.Rulesets.Osu
             new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
         };
 
-        public override IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap)
-        {
-            IEnumerable<HitObject> hitObjects = beatmap.Beatmap.HitObjects;
-            IEnumerable<HitObject> circles = hitObjects.Where(c => !(c is IHasEndTime));
-            IEnumerable<HitObject> sliders = hitObjects.Where(s => s is IHasCurve);
-            IEnumerable<HitObject> spinners = hitObjects.Where(s => s is IHasEndTime && !(s is IHasCurve));
-
-            return new[]
-            {
-                new BeatmapStatistic
-                {
-                    Name = @"Circle Count",
-                    Content = circles.Count().ToString(),
-                    Icon = FontAwesome.fa_circle_o
-                },
-                new BeatmapStatistic
-                {
-                    Name = @"Slider Count",
-                    Content = sliders.Count().ToString(),
-                    Icon = FontAwesome.fa_circle
-                },
-                new BeatmapStatistic
-                {
-                    Name = @"Spinner Count",
-                    Content = spinners.Count().ToString(),
-                    Icon = FontAwesome.fa_circle
-                }
-            };
-        }
-
         public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
         {
             if (mods.HasFlag(LegacyMods.Nightcore))
@@ -181,9 +151,9 @@ namespace osu.Game.Rulesets.Osu
 
         public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
 
-        public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
+        public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
 
-        public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
+        public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
 
         public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
 
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
index 6f2512cc33..4412b6efab 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Replays
             Actions.AddRange(actions);
         }
 
-        public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
+        public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
         {
             Position = legacyFrame.Position;
             if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs
index 8f0feca207..6b9214d9dc 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs
@@ -6,14 +6,13 @@ using System.Collections.Generic;
 using System.Linq;
 using osu.Game.Beatmaps;
 using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Osu.Mods;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Scoring;
 
 namespace osu.Game.Rulesets.Osu.Scoring
 {
-    public class OsuPerformanceCalculator : PerformanceCalculator<OsuHitObject>
+    public class OsuPerformanceCalculator : PerformanceCalculator
     {
         private readonly int countHitCircles;
         private readonly int beatmapMaxCombo;
@@ -27,12 +26,12 @@ namespace osu.Game.Rulesets.Osu.Scoring
         private int count50;
         private int countMiss;
 
-        public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
+        public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
             : base(ruleset, beatmap, score)
         {
             countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
 
-            beatmapMaxCombo = Beatmap.HitObjects.Count;
+            beatmapMaxCombo = Beatmap.HitObjects.Count();
             beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count) + 1;
         }
 
@@ -193,7 +192,5 @@ namespace osu.Game.Rulesets.Osu.Scoring
 
         private double totalHits => count300 + count100 + count50 + countMiss;
         private double totalSuccessfulHits => count300 + count100 + count50;
-
-        protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
     }
 }
diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
index 22c7b719cd..ad1052f86a 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
@@ -7,7 +7,6 @@ using OpenTK;
 using osu.Game.Beatmaps;
 using osu.Game.Input.Handlers;
 using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Beatmaps;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.Osu.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Replays;
@@ -21,17 +20,13 @@ namespace osu.Game.Rulesets.Osu.UI
 {
     public class OsuRulesetContainer : RulesetContainer<OsuHitObject>
     {
-        public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
 
         public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this);
 
-        protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
-
-        protected override BeatmapProcessor<OsuHitObject> CreateBeatmapProcessor() => new OsuBeatmapProcessor();
-
         protected override Playfield CreatePlayfield() => new OsuPlayfield();
 
         public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index aa61f2d60b..33a5e1772e 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -14,18 +14,15 @@ using osu.Game.Tests.Beatmaps;
 
 namespace osu.Game.Rulesets.Taiko.Tests
 {
-    public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
+    public class TaikoBeatmapConversionTest : BeatmapConversionTest<TestTaikoRuleset, ConvertValue>
     {
         protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
 
-        private bool isForCurrentRuleset;
-
         [NonParallelizable]
         [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")]
         [TestCase("slider-generating-drumroll", false)]
-        public void Test(string name, bool isForCurrentRuleset)
+        public new void Test(string name)
         {
-            this.isForCurrentRuleset = isForCurrentRuleset;
             base.Test(name);
         }
 
@@ -43,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
             };
         }
 
-        protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new TaikoBeatmapConverter(isForCurrentRuleset);
+        protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
     }
 
     public struct ConvertValue : IEquatable<ConvertValue>
@@ -70,4 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
                && IsSwell == other.IsSwell
                && IsStrong == other.IsStrong;
     }
+
+    public class TestTaikoRuleset : TaikoRuleset
+    {
+    }
 }
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
index aa7318b863..1bf24a46bc 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
         private Container playfieldContainer;
 
         [BackgroundDependencyLoader]
-        private void load(RulesetStore rulesets)
+        private void load()
         {
             AddStep("Hit!", () => addHitJudgement(false));
             AddStep("Kiai hit", () => addHitJudgement(true));
@@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
                         Title = @"Sample Beatmap",
                         AuthorString = @"peppy",
                     },
+                    Ruleset = new TaikoRuleset().RulesetInfo
                 },
                 ControlPointInfo = controlPointInfo
             });
@@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
                 RelativeSizeAxes = Axes.X,
                 Height = 768,
                 Clock = new FramedClock(rateAdjustClock),
-                Children = new[] { rulesetContainer = new TaikoRulesetContainer(rulesets.GetRuleset(1).CreateInstance(), beatmap, true) }
+                Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) }
             });
         }
 
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs
new file mode 100644
index 0000000000..36f6df7869
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Beatmaps
+{
+    public class TaikoBeatmap : Beatmap<TaikoHitObject>
+    {
+        public override IEnumerable<BeatmapStatistic> GetStatistics()
+        {
+            int hits = HitObjects.Count(s => s is Hit);
+            int drumrolls = HitObjects.Count(s => s is DrumRoll);
+            int swells = HitObjects.Count(s => s is Swell);
+
+            return new[]
+            {
+                new BeatmapStatistic
+                {
+                    Name = @"Hit Count",
+                    Content = hits.ToString(),
+                    Icon = FontAwesome.fa_circle_o
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Drumroll Count",
+                    Content = drumrolls.ToString(),
+                    Icon = FontAwesome.fa_circle
+                },
+                new BeatmapStatistic
+                {
+                    Name = @"Swell Count",
+                    Content = swells.ToString(),
+                    Icon = FontAwesome.fa_circle
+                }
+            };
+        }
+    }
+}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 2f175a9922..d40e8dc643 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -42,12 +42,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
 
         protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(HitObject) };
 
-        public TaikoBeatmapConverter(bool isForCurrentRuleset)
+        public TaikoBeatmapConverter(IBeatmap beatmap)
+            : base(beatmap)
         {
-            this.isForCurrentRuleset = isForCurrentRuleset;
+            isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new TaikoRuleset().RulesetInfo);
         }
 
-        protected override Beatmap<TaikoHitObject> ConvertBeatmap(Beatmap original)
+        protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
         {
             // Rewrite the beatmap info to add the slider velocity multiplier
             BeatmapInfo info = original.BeatmapInfo.DeepClone();
@@ -70,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
             return converted;
         }
 
-        protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, Beatmap beatmap)
+        protected override IEnumerable<TaikoHitObject> ConvertHitObject(HitObject obj, IBeatmap beatmap)
         {
             var distanceData = obj as IHasDistance;
             var repeatsData = obj as IHasRepeats;
@@ -196,5 +197,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
                 }
             }
         }
+
+        protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs
index e510b34ad7..2177a3cbdc 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
             Actions.AddRange(actions);
         }
 
-        public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap)
+        public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap)
         {
             if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim);
             if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim);
diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs
index 58661d7881..f14c53f7ae 100644
--- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs
@@ -2,14 +2,13 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Taiko.Beatmaps;
 using osu.Game.Rulesets.Taiko.Objects;
 using System.Collections.Generic;
 using System;
 
 namespace osu.Game.Rulesets.Taiko
 {
-    internal class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
+    internal class TaikoDifficultyCalculator : DifficultyCalculator
     {
         private const double star_scaling_factor = 0.04125;
 
@@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko
         /// </summary>
         private readonly List<TaikoHitObjectDifficulty> difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
 
-        public TaikoDifficultyCalculator(Beatmap beatmap)
+        public TaikoDifficultyCalculator(IBeatmap beatmap)
             : base(beatmap)
         {
         }
@@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko
             difficultyHitObjects.Clear();
 
             foreach (var hitObject in Beatmap.HitObjects)
-                difficultyHitObjects.Add(new TaikoHitObjectDifficulty(hitObject));
+                difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject));
 
             // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
             difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
@@ -132,7 +131,5 @@ namespace osu.Game.Rulesets.Taiko
 
             return difficulty;
         }
-
-        protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter(Beatmap beatmap) => new TaikoBeatmapConverter(true);
     }
 }
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 06a8e44a14..102de5717f 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -13,12 +13,14 @@ using osu.Framework.Input.Bindings;
 using osu.Game.Rulesets.Replays.Types;
 using osu.Game.Rulesets.Taiko.Replays;
 using osu.Game.Beatmaps.Legacy;
+using osu.Game.Rulesets.Taiko.Beatmaps;
 
 namespace osu.Game.Rulesets.Taiko
 {
     public class TaikoRuleset : Ruleset
     {
-        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoRulesetContainer(this, beatmap, isForCurrentRuleset);
+        public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new TaikoRulesetContainer(this, beatmap);
+        public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
 
         public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
         {
@@ -140,7 +142,7 @@ namespace osu.Game.Rulesets.Taiko
 
         public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
 
-        public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap);
+        public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap);
 
         public override int? LegacyID => 1;
 
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
index 3d3c6ab2f3..313c205981 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Replays;
 using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Beatmaps;
 using osu.Game.Rulesets.Taiko.Objects;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.Scoring;
@@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.UI
 {
     public class TaikoRulesetContainer : ScrollingRulesetContainer<TaikoPlayfield, TaikoHitObject>
     {
-        public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
 
@@ -93,8 +92,6 @@ namespace osu.Game.Rulesets.Taiko.UI
 
         public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
 
-        protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset);
-
         public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
 
         protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo)
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 6453cdbd3e..f60caf2397 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -275,13 +275,13 @@ namespace osu.Game.Tests.Beatmaps.IO
                 Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID));
             Assert.IsTrue(set.Beatmaps.Count > 0);
             var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Count > 0);
+            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
             beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Count > 0);
+            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
             beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Count > 0);
+            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
             beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
-            Assert.IsTrue(beatmap?.HitObjects.Count > 0);
+            Assert.IsTrue(beatmap?.HitObjects.Any() == true);
         }
 
         private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
index 886c1120d4..0d3e08154f 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
@@ -12,8 +12,12 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Beatmaps;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
+using osu.Game.Rulesets.Mania;
 using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
 using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Taiko;
 using osu.Game.Screens.Select;
 using osu.Game.Tests.Beatmaps;
 
@@ -24,7 +28,7 @@ namespace osu.Game.Tests.Visual
     {
         private RulesetStore rulesets;
         private TestBeatmapInfoWedge infoWedge;
-        private readonly List<Beatmap> beatmaps = new List<Beatmap>();
+        private readonly List<IBeatmap> beatmaps = new List<IBeatmap>();
         private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
 
         [BackgroundDependencyLoader]
@@ -72,13 +76,23 @@ namespace osu.Game.Tests.Visual
 
                 selectBeatmap(testBeatmap);
 
+                testBeatmapLabels(ruleset);
+
                 // TODO: adjust cases once more info is shown for other gamemodes
                 switch (ruleset)
                 {
-                    case OsuRuleset osu:
-                        testOsuBeatmap(osu);
+                    case OsuRuleset _:
                         testInfoLabels(5);
                         break;
+                    case TaikoRuleset _:
+                        testInfoLabels(5);
+                        break;
+                    case CatchRuleset _:
+                        testInfoLabels(5);
+                        break;
+                    case ManiaRuleset _:
+                        testInfoLabels(4);
+                        break;
                     default:
                         testInfoLabels(2);
                         break;
@@ -88,7 +102,7 @@ namespace osu.Game.Tests.Visual
             testNullBeatmap();
         }
 
-        private void testOsuBeatmap(OsuRuleset ruleset)
+        private void testBeatmapLabels(Ruleset ruleset)
         {
             AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version");
             AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
@@ -112,7 +126,7 @@ namespace osu.Game.Tests.Visual
             AddAssert("check no infolabels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
         }
 
-        private void selectBeatmap(Beatmap b)
+        private void selectBeatmap(IBeatmap b)
         {
             BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
 
@@ -134,11 +148,11 @@ namespace osu.Game.Tests.Visual
             });
         }
 
-        private Beatmap createTestBeatmap(RulesetInfo ruleset)
+        private IBeatmap createTestBeatmap(RulesetInfo ruleset)
         {
             List<HitObject> objects = new List<HitObject>();
             for (double i = 0; i < 50000; i += 1000)
-                objects.Add(new HitObject { StartTime = i });
+                objects.Add(new TestHitObject { StartTime = i });
 
             return new Beatmap
             {
@@ -153,7 +167,8 @@ namespace osu.Game.Tests.Visual
                     },
                     Ruleset = ruleset,
                     StarDifficulty = 6,
-                    Version = $"{ruleset.ShortName}Version"
+                    Version = $"{ruleset.ShortName}Version",
+                    BaseDifficulty = new BeatmapDifficulty()
                 },
                 HitObjects = objects
             };
@@ -163,5 +178,12 @@ namespace osu.Game.Tests.Visual
         {
             public new BufferedWedgeInfo Info => base.Info;
         }
+
+        private class TestHitObject : HitObject, IHasPosition
+        {
+            public float X { get; } = 0;
+            public float Y { get; } = 0;
+            public Vector2 Position { get; } = Vector2.Zero;
+        }
     }
 }
diff --git a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
index a6a6130a8f..442ca1322a 100644
--- a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs
@@ -1,7 +1,6 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using osu.Framework.Timing;
 using osu.Game.Beatmaps.Timing;
 using System.Collections.Generic;
 using NUnit.Framework;
@@ -16,8 +15,6 @@ namespace osu.Game.Tests.Visual
 
         public TestCaseBreakOverlay()
         {
-            Clock = new FramedClock();
-
             Child = breakOverlay = new BreakOverlay(true);
 
             AddStep("2s break", () => startBreak(2000));
diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs
index 25f8ba06c4..bb5bf93a69 100644
--- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs
+++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers;
 using osu.Game.Beatmaps;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Rulesets;
-using osu.Game.Screens.Multiplayer;
+using osu.Game.Screens.Multi.Components;
 using osu.Game.Users;
 
 namespace osu.Game.Tests.Visual
diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs
index 582ab5ecc9..f037d70493 100644
--- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs
@@ -332,7 +332,7 @@ namespace osu.Game.Tests.Visual
 
             private readonly Drawable tracker;
 
-            public TimingPointVisualiser(Beatmap beatmap, double length)
+            public TimingPointVisualiser(IBeatmap beatmap, double length)
             {
                 this.length = length;
 
diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs
index 6ba671c7fc..5bc16fe420 100644
--- a/osu.Game.Tests/Visual/TestCaseReplay.cs
+++ b/osu.Game.Tests/Visual/TestCaseReplay.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual
             // 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.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
-            var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo));
+            var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap);
 
             // We have the replay
             var replay = dummyRulesetContainer.Replay;
diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs
index 88059d2dcf..cb1425ca69 100644
--- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs
+++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs
@@ -7,7 +7,7 @@ using osu.Framework.Graphics;
 using osu.Game.Beatmaps;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Rulesets;
-using osu.Game.Screens.Multiplayer;
+using osu.Game.Screens.Multi.Components;
 using osu.Game.Users;
 
 namespace osu.Game.Tests.Visual
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index 12a017f68c..84897853d8 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -15,21 +15,27 @@ namespace osu.Game.Beatmaps
     /// <summary>
     /// A Beatmap containing converted HitObjects.
     /// </summary>
-    public class Beatmap<T> : IJsonSerializable
+    public class Beatmap<T> : IBeatmap
         where T : HitObject
     {
-        public BeatmapInfo BeatmapInfo = new BeatmapInfo();
-        public ControlPointInfo ControlPointInfo = new ControlPointInfo();
-        public List<BreakPeriod> Breaks = new List<BreakPeriod>();
+        public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo
+        {
+            Metadata = new BeatmapMetadata
+            {
+                Artist = @"Unknown",
+                Title = @"Unknown",
+                AuthorString = @"Unknown Creator",
+            },
+            Version = @"Normal",
+            BaseDifficulty = new BeatmapDifficulty()
+        };
 
         [JsonIgnore]
         public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata;
 
-        /// <summary>
-        /// The HitObjects this Beatmap contains.
-        /// </summary>
-        [JsonConverter(typeof(TypedListConverter<HitObject>))]
-        public List<T> HitObjects = new List<T>();
+        public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo();
+
+        public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>();
 
         /// <summary>
         /// Total amount of break time in the beatmap.
@@ -38,51 +44,28 @@ namespace osu.Game.Beatmaps
         public double TotalBreakTime => Breaks.Sum(b => b.Duration);
 
         /// <summary>
-        /// Constructs a new beatmap.
+        /// The HitObjects this Beatmap contains.
         /// </summary>
-        /// <param name="original">The original beatmap to use the parameters of.</param>
-        public Beatmap(Beatmap<T> original = null)
-        {
-            BeatmapInfo = original?.BeatmapInfo.DeepClone() ?? BeatmapInfo;
-            ControlPointInfo = original?.ControlPointInfo ?? ControlPointInfo;
-            Breaks = original?.Breaks ?? Breaks;
-            HitObjects = original?.HitObjects ?? HitObjects;
+        [JsonConverter(typeof(TypedListConverter<HitObject>))]
+        public List<T> HitObjects = new List<T>();
 
-            if (original == null && Metadata == null)
-            {
-                // we may have no metadata in cases we weren't sourced from the database.
-                // let's fill it (and other related fields) so we don't need to null-check it in future usages.
-                BeatmapInfo = new BeatmapInfo
-                {
-                    Metadata = new BeatmapMetadata
-                    {
-                        Artist = @"Unknown",
-                        Title = @"Unknown",
-                        AuthorString = @"Unknown Creator",
-                    },
-                    Version = @"Normal",
-                    BaseDifficulty = new BeatmapDifficulty()
-                };
-            }
+        IEnumerable<HitObject> IBeatmap.HitObjects => HitObjects;
+
+        public virtual IEnumerable<BeatmapStatistic> GetStatistics() => Enumerable.Empty<BeatmapStatistic>();
+
+        IBeatmap IBeatmap.Clone() => Clone();
+
+        public Beatmap<T> Clone()
+        {
+            var newInstance = (Beatmap<T>)MemberwiseClone();
+            newInstance.BeatmapInfo = BeatmapInfo.DeepClone();
+
+            return newInstance;
         }
     }
 
-    /// <summary>
-    /// A Beatmap containing un-converted HitObjects.
-    /// </summary>
     public class Beatmap : Beatmap<HitObject>
     {
-        /// <summary>
-        /// Constructs a new beatmap.
-        /// </summary>
-        /// <param name="original">The original beatmap to use the parameters of.</param>
-        public Beatmap(Beatmap original)
-            : base(original)
-        {
-        }
-
-        public Beatmap()
-        {
-        }
+        public Beatmap Clone() => (Beatmap)base.Clone();
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index 153cace187..b7a454460f 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -22,32 +22,34 @@ namespace osu.Game.Beatmaps
             remove => ObjectConverted -= value;
         }
 
-        /// <summary>
-        /// Checks if a Beatmap can be converted using this Beatmap Converter.
-        /// </summary>
-        /// <param name="beatmap">The Beatmap to check.</param>
-        /// <returns>Whether the Beatmap can be converted using this Beatmap Converter.</returns>
-        public bool CanConvert(Beatmap beatmap) => ValidConversionTypes.All(t => beatmap.HitObjects.Any(t.IsInstanceOfType));
+        public IBeatmap Beatmap { get; }
 
-        /// <summary>
-        /// Converts a Beatmap using this Beatmap Converter.
-        /// </summary>
-        /// <param name="original">The un-converted Beatmap.</param>
-        /// <returns>The converted Beatmap.</returns>
-        public Beatmap<T> Convert(Beatmap original)
+        protected BeatmapConverter(IBeatmap beatmap)
         {
-            // We always operate on a clone of the original beatmap, to not modify it game-wide
-            return ConvertBeatmap(new Beatmap(original));
+            Beatmap = beatmap;
         }
 
-        void IBeatmapConverter.Convert(Beatmap original) => Convert(original);
+        /// <summary>
+        /// Whether <see cref="Beatmap"/> can be converted by this <see cref="BeatmapConverter{T}"/>.
+        /// </summary>
+        public bool CanConvert => !Beatmap.HitObjects.Any() || ValidConversionTypes.All(t => Beatmap.HitObjects.Any(t.IsInstanceOfType));
+
+        /// <summary>
+        /// Converts <see cref="Beatmap"/>.
+        /// </summary>
+        /// <returns>The converted Beatmap.</returns>
+        public IBeatmap Convert()
+        {
+            // We always operate on a clone of the original beatmap, to not modify it game-wide
+            return ConvertBeatmap(Beatmap.Clone());
+        }
 
         /// <summary>
         /// Performs the conversion of a Beatmap using this Beatmap Converter.
         /// </summary>
         /// <param name="original">The un-converted Beatmap.</param>
         /// <returns>The converted Beatmap.</returns>
-        protected virtual Beatmap<T> ConvertBeatmap(Beatmap original)
+        protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
         {
             var beatmap = CreateBeatmap();
 
@@ -67,7 +69,7 @@ namespace osu.Game.Beatmaps
         /// <param name="original">The hit object to convert.</param>
         /// <param name="beatmap">The un-converted Beatmap.</param>
         /// <returns>The converted hit object.</returns>
-        private IEnumerable<T> convert(HitObject original, Beatmap beatmap)
+        private IEnumerable<T> convert(HitObject original, IBeatmap beatmap)
         {
             // Check if the hitobject is already the converted type
             T tObject = original as T;
@@ -107,6 +109,6 @@ namespace osu.Game.Beatmaps
         /// <param name="original">The hit object to convert.</param>
         /// <param name="beatmap">The un-converted Beatmap.</param>
         /// <returns>The converted hit object.</returns>
-        protected abstract IEnumerable<T> ConvertHitObject(HitObject original, Beatmap beatmap);
+        protected abstract IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap);
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 645e52a6c6..36fde8a319 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -9,7 +9,9 @@ using System.Linq.Expressions;
 using System.Threading.Tasks;
 using Microsoft.EntityFrameworkCore;
 using osu.Framework.Audio;
+using osu.Framework.Audio.Track;
 using osu.Framework.Extensions;
+using osu.Framework.Graphics.Textures;
 using osu.Framework.Logging;
 using osu.Framework.Platform;
 using osu.Game.Beatmaps.Formats;
@@ -333,7 +335,7 @@ namespace osu.Game.Beatmaps
                     ms.Position = 0;
 
                     var decoder = Decoder.GetDecoder<Beatmap>(sr);
-                    Beatmap beatmap = decoder.Decode(sr);
+                    IBeatmap beatmap = decoder.Decode(sr);
 
                     beatmap.BeatmapInfo.Path = name;
                     beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
@@ -341,9 +343,16 @@ namespace osu.Game.Beatmaps
 
                     RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
 
-                    // TODO: this should be done in a better place once we actually need to dynamically update it.
                     beatmap.BeatmapInfo.Ruleset = ruleset;
-                    beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
+
+                    if (ruleset != null)
+                    {
+                        // TODO: this should be done in a better place once we actually need to dynamically update it.
+                        var converted = new DummyConversionBeatmap(beatmap).GetPlayableBeatmap(ruleset);
+                        beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(converted).Calculate();
+                    }
+                    else
+                        beatmap.BeatmapInfo.StarDifficulty = 0;
 
                     beatmapInfos.Add(beatmap.BeatmapInfo);
                 }
@@ -351,5 +360,23 @@ namespace osu.Game.Beatmaps
 
             return beatmapInfos;
         }
+
+        /// <summary>
+        /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
+        /// </summary>
+        private class DummyConversionBeatmap : WorkingBeatmap
+        {
+            private readonly IBeatmap beatmap;
+
+            public DummyConversionBeatmap(IBeatmap beatmap)
+                : base(beatmap.BeatmapInfo)
+            {
+                this.beatmap = beatmap;
+            }
+
+            protected override IBeatmap GetBeatmap() => beatmap;
+            protected override Texture GetBackground() => null;
+            protected override Track GetTrack() => null;
+        }
     }
 }
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 8e09d66c42..71406c6034 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps
                 this.audioManager = audioManager;
             }
 
-            protected override Beatmap GetBeatmap()
+            protected override IBeatmap GetBeatmap()
             {
                 try
                 {
diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs
index 8f5a2a4cab..bf1cd7d4ee 100644
--- a/osu.Game/Beatmaps/BeatmapProcessor.cs
+++ b/osu.Game/Beatmaps/BeatmapProcessor.cs
@@ -2,30 +2,47 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using System.Linq;
-using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Types;
 
 namespace osu.Game.Beatmaps
 {
+    public interface IBeatmapProcessor
+    {
+        IBeatmap Beatmap { get; }
+
+        /// <summary>
+        /// Post-processes <see cref="Beatmap"/> to add mode-specific components that aren't added during conversion.
+        /// <para>
+        /// An example of such a usage is for combo colours.
+        /// </para>
+        /// </summary>
+        void PostProcess();
+    }
+
     /// <summary>
     /// Processes a post-converted Beatmap.
     /// </summary>
     /// <typeparam name="TObject">The type of HitObject contained in the Beatmap.</typeparam>
-    public class BeatmapProcessor<TObject>
-        where TObject : HitObject
+    public class BeatmapProcessor : IBeatmapProcessor
     {
+        public IBeatmap Beatmap { get; }
+
+        public BeatmapProcessor(IBeatmap beatmap)
+        {
+            Beatmap = beatmap;
+        }
+
         /// <summary>
         /// Post-processes a Beatmap to add mode-specific components that aren't added during conversion.
         /// <para>
         /// An example of such a usage is for combo colours.
         /// </para>
         /// </summary>
-        /// <param name="beatmap">The Beatmap to process.</param>
-        public virtual void PostProcess(Beatmap<TObject> beatmap)
+        public virtual void PostProcess()
         {
             IHasComboInformation lastObj = null;
 
-            foreach (var obj in beatmap.HitObjects.OfType<IHasComboInformation>())
+            foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
             {
                 if (obj.NewCombo)
                 {
diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs
index 5e2d9afd23..37155c09cd 100644
--- a/osu.Game/Beatmaps/DifficultyCalculator.cs
+++ b/osu.Game/Beatmaps/DifficultyCalculator.cs
@@ -1,7 +1,6 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using osu.Game.Rulesets.Objects;
 using System.Collections.Generic;
 using osu.Game.Rulesets.Mods;
 using osu.Framework.Timing;
@@ -12,30 +11,17 @@ namespace osu.Game.Beatmaps
 {
     public abstract class DifficultyCalculator
     {
-        protected double TimeRate = 1;
-
-        public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
-    }
-
-    public abstract class DifficultyCalculator<T> : DifficultyCalculator where T : HitObject
-    {
-        protected readonly Beatmap<T> Beatmap;
+        protected readonly IBeatmap Beatmap;
         protected readonly Mod[] Mods;
 
-        protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null)
+        protected double TimeRate = 1;
+
+        protected DifficultyCalculator(IBeatmap beatmap, Mod[] mods = null)
         {
+            Beatmap = beatmap;
             Mods = mods ?? new Mod[0];
 
-            var converter = CreateBeatmapConverter(beatmap);
-
-            foreach (var mod in Mods.OfType<IApplicableToBeatmapConverter<T>>())
-                mod.ApplyToBeatmapConverter(converter);
-
-            Beatmap = converter.Convert(beatmap);
-
             ApplyMods(Mods);
-
-            PreprocessHitObjects();
         }
 
         protected virtual void ApplyMods(Mod[] mods)
@@ -43,22 +29,12 @@ namespace osu.Game.Beatmaps
             var clock = new StopwatchClock();
             mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
             TimeRate = clock.Rate;
-
-            foreach (var mod in Mods.OfType<IApplicableToDifficulty>())
-                mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty);
-
-            foreach (var h in Beatmap.HitObjects)
-                h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty);
-
-            foreach (var mod in mods.OfType<IApplicableToHitObject<T>>())
-                foreach (var obj in Beatmap.HitObjects)
-                    mod.ApplyToHitObject(obj);
         }
 
         protected virtual void PreprocessHitObjects()
         {
         }
 
-        protected abstract BeatmapConverter<T> CreateBeatmapConverter(Beatmap beatmap);
+        public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
     }
 }
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 0424ff84f1..da52dc7284 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -7,6 +7,7 @@ using osu.Framework.Audio.Track;
 using osu.Framework.Graphics.Textures;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.UI;
 
 namespace osu.Game.Beatmaps
@@ -39,7 +40,7 @@ namespace osu.Game.Beatmaps
             this.game = game;
         }
 
-        protected override Beatmap GetBeatmap() => new Beatmap();
+        protected override IBeatmap GetBeatmap() => new Beatmap();
 
         protected override Texture GetBackground() => game.Textures.Get(@"Backgrounds/bg4");
 
@@ -53,12 +54,14 @@ namespace osu.Game.Beatmaps
             {
                 public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] { };
 
-                public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset)
+                public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap)
                 {
                     throw new NotImplementedException();
                 }
 
-                public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => null;
+                public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
+
+                public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => null;
 
                 public override string Description => "dummy";
 
@@ -68,6 +71,14 @@ namespace osu.Game.Beatmaps
                     : base(rulesetInfo)
                 {
                 }
+
+                private class DummyBeatmapConverter : IBeatmapConverter
+                {
+                    public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
+                    public IBeatmap Beatmap { get; set; }
+                    public bool CanConvert => true;
+                    public IBeatmap Convert() => Beatmap;
+                }
             }
         }
     }
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
new file mode 100644
index 0000000000..fe20bce98a
--- /dev/null
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -0,0 +1,56 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.IO.Serialization;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Beatmaps
+{
+    public interface IBeatmap : IJsonSerializable
+    {
+        /// <summary>
+        /// This beatmap's info.
+        /// </summary>
+        BeatmapInfo BeatmapInfo { get; set; }
+
+        /// <summary>
+        /// This beatmap's metadata.
+        /// </summary>
+        BeatmapMetadata Metadata { get; }
+
+        /// <summary>
+        /// The control points in this beatmap.
+        /// </summary>
+        ControlPointInfo ControlPointInfo { get; }
+
+        /// <summary>
+        /// The breaks in this beatmap.
+        /// </summary>
+        List<BreakPeriod> Breaks { get; }
+
+        /// <summary>
+        /// Total amount of break time in the beatmap.
+        /// </summary>
+        double TotalBreakTime { get; }
+
+        /// <summary>
+        /// The hitobjects contained by this beatmap.
+        /// </summary>
+        IEnumerable<HitObject> HitObjects { get; }
+
+        /// <summary>
+        /// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
+        /// </summary>
+        /// <returns></returns>
+        IEnumerable<BeatmapStatistic> GetStatistics();
+
+        /// <summary>
+        /// Creates a shallow-clone of this beatmap and returns it.
+        /// </summary>
+        /// <returns>The shallow-cloned beatmap.</returns>
+        IBeatmap Clone();
+    }
+}
diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs
index 6c25395a56..00566093b8 100644
--- a/osu.Game/Beatmaps/IBeatmapConverter.cs
+++ b/osu.Game/Beatmaps/IBeatmapConverter.cs
@@ -16,10 +16,16 @@ namespace osu.Game.Beatmaps
         /// </summary>
         event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
 
+        IBeatmap Beatmap { get; }
+
         /// <summary>
-        /// Converts a Beatmap using this Beatmap Converter.
+        /// Whether <see cref="Beatmap"/> can be converted by this <see cref="IBeatmapConverter"/>.
         /// </summary>
-        /// <param name="beatmap">The un-converted Beatmap.</param>
-        void Convert(Beatmap beatmap);
+        bool CanConvert { get; }
+
+        /// <summary>
+        /// Converts <see cref="Beatmap"/>.
+        /// </summary>
+        IBeatmap Convert();
     }
 }
diff --git a/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs b/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs
deleted file mode 100644
index eea82dac6d..0000000000
--- a/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-namespace osu.Game.Beatmaps.Legacy
-{
-    /// <summary>
-    /// A type of Beatmap loaded from a legacy .osu beatmap file (version &lt;=15).
-    /// </summary>
-    public class LegacyBeatmap : Beatmap
-    {
-        /// <summary>
-        /// Constructs a new beatmap.
-        /// </summary>
-        /// <param name="original">The original beatmap to use the parameters of.</param>
-        internal LegacyBeatmap(Beatmap original = null)
-            : base(original)
-        {
-            HitObjects = original?.HitObjects;
-        }
-    }
-}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 4080e34e81..9c389bbb8f 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -14,6 +14,8 @@ using osu.Framework.IO.File;
 using System.IO;
 using osu.Game.IO.Serialization;
 using System.Diagnostics;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.UI;
 using osu.Game.Skinning;
 
 namespace osu.Game.Beatmaps
@@ -36,7 +38,7 @@ namespace osu.Game.Beatmaps
 
             Mods.ValueChanged += mods => applyRateAdjustments();
 
-            beatmap = new AsyncLazy<Beatmap>(populateBeatmap);
+            beatmap = new AsyncLazy<IBeatmap>(populateBeatmap);
             background = new AsyncLazy<Texture>(populateBackground, b => b == null || !b.IsDisposed);
             track = new AsyncLazy<Track>(populateTrack);
             waveform = new AsyncLazy<Waveform>(populateWaveform);
@@ -45,7 +47,7 @@ namespace osu.Game.Beatmaps
         }
 
         /// <summary>
-        /// Saves the <see cref="Beatmap"/>.
+        /// Saves the <see cref="Beatmaps.Beatmap"/>.
         /// </summary>
         public void Save()
         {
@@ -55,7 +57,7 @@ namespace osu.Game.Beatmaps
             Process.Start(path);
         }
 
-        protected abstract Beatmap GetBeatmap();
+        protected abstract IBeatmap GetBeatmap();
         protected abstract Texture GetBackground();
         protected abstract Track GetTrack();
         protected virtual Skin GetSkin() => new DefaultSkin();
@@ -63,12 +65,11 @@ namespace osu.Game.Beatmaps
         protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
 
         public bool BeatmapLoaded => beatmap.IsResultAvailable;
-        public Beatmap Beatmap => beatmap.Value.Result;
-        public async Task<Beatmap> GetBeatmapAsync() => await beatmap.Value;
+        public IBeatmap Beatmap => beatmap.Value.Result;
+        public async Task<IBeatmap> GetBeatmapAsync() => await beatmap.Value;
+        private readonly AsyncLazy<IBeatmap> beatmap;
 
-        private readonly AsyncLazy<Beatmap> beatmap;
-
-        private Beatmap populateBeatmap()
+        private IBeatmap populateBeatmap()
         {
             var b = GetBeatmap() ?? new Beatmap();
 
@@ -78,6 +79,51 @@ namespace osu.Game.Beatmaps
             return b;
         }
 
+        /// <summary>
+        /// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
+        /// <para>
+        /// The returned <see cref="IBeatmap"/> is in a playable state - all <see cref="HitObject"/> and <see cref="BeatmapDifficulty"/> <see cref="Mod"/>s
+        /// have been applied, and <see cref="HitObject"/>s have been fully constructed.
+        /// </para>
+        /// </summary>
+        /// <param name="ruleset">The <see cref="RulesetInfo"/> to create a playable <see cref="IBeatmap"/> for.</param>
+        /// <returns>The converted <see cref="IBeatmap"/>.</returns>
+        /// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
+        public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset)
+        {
+            var rulesetInstance = ruleset.CreateInstance();
+
+            IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap);
+
+            // Check if the beatmap can be converted
+            if (!converter.CanConvert)
+                throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
+
+            // Apply conversion mods
+            foreach (var mod in Mods.Value.OfType<IApplicableToBeatmapConverter>())
+                mod.ApplyToBeatmapConverter(converter);
+
+            // Convert
+            IBeatmap converted = converter.Convert();
+
+            // Apply difficulty mods
+            foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
+                mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty);
+
+            // Post-process
+            rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess();
+
+            // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
+            foreach (var obj in converted.HitObjects)
+                obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
+
+            foreach (var mod in Mods.Value.OfType<IApplicableToHitObject>())
+            foreach (var obj in converted.HitObjects)
+                mod.ApplyToHitObject(obj);
+
+            return converted;
+        }
+
         public bool BackgroundLoaded => background.IsResultAvailable;
         public Texture Background => background.Value.Result;
         public async Task<Texture> GetBackgroundAsync() => await background.Value;
diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
index 3f59eeca97..f5017de639 100644
--- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
@@ -14,14 +14,18 @@ namespace osu.Game.Graphics.UserInterface
     public class BreadcrumbControl<T> : OsuTabControl<T>
     {
         private const float padding = 10;
+        private const float item_chevron_size = 10;
 
-        protected override TabItem<T> CreateTabItem(T value) => new BreadcrumbTabItem(value);
+        protected override TabItem<T> CreateTabItem(T value) => new BreadcrumbTabItem(value)
+        {
+            AccentColour = AccentColour,
+        };
 
-        protected override float StripWidth() => base.StripWidth() - (padding + 8);
+        protected override float StripWidth() => base.StripWidth() - (padding + item_chevron_size);
 
         public BreadcrumbControl()
         {
-            Height = 26;
+            Height = 32;
             TabContainer.Spacing = new Vector2(padding, 0f);
             Current.ValueChanged += tab =>
             {
@@ -47,6 +51,7 @@ namespace osu.Game.Graphics.UserInterface
 
             public override bool HandleKeyboardInput => State == Visibility.Visible;
             public override bool HandleMouseInput => State == Visibility.Visible;
+            public override bool IsRemovable => true;
 
             private Visibility state;
 
@@ -77,13 +82,14 @@ namespace osu.Game.Graphics.UserInterface
 
             public BreadcrumbTabItem(T value) : base(value)
             {
-                Text.TextSize = 16;
-                Padding = new MarginPadding { Right = padding + 8 }; //padding + chevron width
+                Text.TextSize = 18;
+                Text.Margin = new MarginPadding { Vertical = 8 };
+                Padding = new MarginPadding { Right = padding + item_chevron_size };
                 Add(Chevron = new SpriteIcon
                 {
                     Anchor = Anchor.CentreRight,
                     Origin = Anchor.CentreLeft,
-                    Size = new Vector2(12),
+                    Size = new Vector2(item_chevron_size),
                     Icon = FontAwesome.fa_chevron_right,
                     Margin = new MarginPadding { Left = padding },
                     Alpha = 0f,
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 331dcec4c0..a2542c537f 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -12,10 +12,8 @@ using osu.Framework.Configuration;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Transforms;
 using osu.Framework.Graphics.UserInterface;
 using osu.Framework.Input;
-using osu.Framework.MathUtils;
 using osu.Framework.Threading;
 using osu.Game.Configuration;
 using osu.Game.Graphics;
@@ -181,7 +179,7 @@ namespace osu.Game.Overlays
                 {
                     textbox.HoldFocus = false;
                     if (1f - ChatHeight.Value < channel_selection_min_height)
-                        transformChatHeightTo(1f - channel_selection_min_height, 800, Easing.OutQuint);
+                        this.TransformBindableTo(ChatHeight, 1f - channel_selection_min_height, 800, Easing.OutQuint);
                 }
                 else
                     textbox.HoldFocus = true;
@@ -533,26 +531,5 @@ namespace osu.Game.Overlays
 
             api.Queue(req);
         }
-
-        private void transformChatHeightTo(double newChatHeight, double duration = 0, Easing easing = Easing.None)
-        {
-            this.TransformTo(this.PopulateTransform(new TransformChatHeight(), newChatHeight, duration, easing));
-        }
-
-        private class TransformChatHeight : Transform<double, ChatOverlay>
-        {
-            private double valueAt(double time)
-            {
-                if (time < StartTime) return StartValue;
-                if (time >= EndTime) return EndValue;
-
-                return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
-            }
-
-            public override string TargetMember => "ChatHeight.Value";
-
-            protected override void Apply(ChatOverlay d, double time) => d.ChatHeight.Value = valueAt(time);
-            protected override void ReadIntoStartValue(ChatOverlay d) => StartValue = d.ChatHeight.Value;
-        }
     }
 }
diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs
index 8883dfdebb..4f4348c131 100644
--- a/osu.Game/Overlays/Direct/FilterControl.cs
+++ b/osu.Game/Overlays/Direct/FilterControl.cs
@@ -1,8 +1,6 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK;
-using OpenTK.Graphics;
 using osu.Framework.Allocation;
 using osu.Framework.Configuration;
 using osu.Framework.Graphics;
@@ -12,6 +10,8 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Online.API.Requests;
 using osu.Game.Overlays.SearchableList;
 using osu.Game.Rulesets;
+using OpenTK;
+using OpenTK.Graphics;
 
 namespace osu.Game.Overlays.Direct
 {
@@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Direct
 
         protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552");
         protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked;
+
         protected override Drawable CreateSupplementaryControls()
         {
             modeButtons = new FillFlowContainer<RulesetToggleButton>
@@ -38,7 +39,7 @@ namespace osu.Game.Overlays.Direct
         {
             DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark;
 
-            Ruleset.BindTo(game?.Ruleset ?? new Bindable<RulesetInfo> { Value = rulesets.GetRuleset(0) });
+            Ruleset.Value = game?.Ruleset.Value ?? rulesets.GetRuleset(0);
             foreach (var r in rulesets.AvailableRulesets)
             {
                 modeButtons.Add(new RulesetToggleButton(Ruleset, r));
@@ -49,14 +50,15 @@ namespace osu.Game.Overlays.Direct
         {
             private Drawable icon
             {
-                get { return iconContainer.Icon; }
-                set { iconContainer.Icon = value; }
+                get => iconContainer.Icon;
+                set => iconContainer.Icon = value;
             }
 
             private RulesetInfo ruleset;
+
             public RulesetInfo Ruleset
             {
-                get { return ruleset; }
+                get => ruleset;
                 set
                 {
                     ruleset = value;
@@ -73,6 +75,9 @@ namespace osu.Game.Overlays.Direct
                 iconContainer.FadeTo(Ruleset.ID == obj?.ID ? 1f : 0.5f, 100);
             }
 
+            public override bool HandleKeyboardInput => !bindable.Disabled && base.HandleKeyboardInput;
+            public override bool HandleMouseInput => !bindable.Disabled && base.HandleMouseInput;
+
             public RulesetToggleButton(Bindable<RulesetInfo> bindable, RulesetInfo ruleset)
             {
                 this.bindable = bindable;
diff --git a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs
index 05866f7002..1da51e4a5a 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs
@@ -87,8 +87,8 @@ namespace osu.Game.Overlays.Toolbar
                 ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault();
         }
 
-        public override bool HandleKeyboardInput => !ruleset.Disabled;
-        public override bool HandleMouseInput => !ruleset.Disabled;
+        public override bool HandleKeyboardInput => !ruleset.Disabled && base.HandleKeyboardInput;
+        public override bool HandleMouseInput => !ruleset.Disabled && base.HandleMouseInput;
 
         private void disabledChanged(bool isDisabled) => this.FadeColour(isDisabled ? Color4.Gray : Color4.White, 300);
 
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 1820053d3d..5f1b9a6bad 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Edit
 
         private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool;
 
-        protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true);
+        protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap);
 
         protected abstract IReadOnlyList<ICompositionTool> CompositionTools { get; }
 
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
index a03a003810..1b8e62b53c 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs
@@ -10,13 +10,12 @@ namespace osu.Game.Rulesets.Mods
     /// Interface for a <see cref="Mod"/> that applies changes to a <see cref="BeatmapConverter{TObject}"/>.
     /// </summary>
     /// <typeparam name="TObject">The type of converted <see cref="HitObject"/>.</typeparam>
-    public interface IApplicableToBeatmapConverter<TObject> : IApplicableMod
-        where TObject : HitObject
+    public interface IApplicableToBeatmapConverter : IApplicableMod
     {
         /// <summary>
         /// Applies this <see cref="Mod"/> to a <see cref="BeatmapConverter{TObject}"/>.
         /// </summary>
         /// <param name="beatmapConverter">The <see cref="BeatmapConverter{TObject}"/> to apply to.</param>
-        void ApplyToBeatmapConverter(BeatmapConverter<TObject> beatmapConverter);
+        void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter);
     }
 }
diff --git a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
index 0fd2e398c8..d6f330d9df 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs
@@ -8,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
     /// <summary>
     /// An interface for <see cref="Mod"/>s that can be applied to <see cref="HitObject"/>s.
     /// </summary>
-    public interface IApplicableToHitObject<in TObject> : IApplicableMod
-        where TObject : HitObject
+    public interface IApplicableToHitObject : IApplicableMod
     {
         /// <summary>
         /// Applies this <see cref="IApplicableToHitObject{TObject}"/> to a <see cref="HitObject"/>.
         /// </summary>
         /// <param name="hitObject">The <see cref="HitObject"/> to apply to.</param>
-        void ApplyToHitObject(TObject hitObject);
+        void ApplyToHitObject(HitObject hitObject);
     }
 }
diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
index adf35ce078..fdd528f296 100644
--- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
+++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
@@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Replays.Types
         /// </summary>
         /// <param name="legacyFrame">The <see cref="LegacyReplayFrame"/> to extract values from.</param>
         /// <param name="beatmap">The beatmap.</param>
-        void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap);
+        void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap);
     }
 }
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index cd1d030afe..6883d319f4 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -22,8 +22,6 @@ namespace osu.Game.Rulesets
     {
         public readonly RulesetInfo RulesetInfo;
 
-        public virtual IEnumerable<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap) => new BeatmapStatistic[] { };
-
         public IEnumerable<Mod> GetAllMods() => Enum.GetValues(typeof(ModType)).Cast<ModType>()
                                                     // Confine all mods of each mod type into a single IEnumerable<Mod>
                                                     .SelectMany(GetModsFor)
@@ -52,14 +50,17 @@ namespace osu.Game.Rulesets
         /// Attempt to create a hit renderer for a beatmap
         /// </summary>
         /// <param name="beatmap">The beatmap to create the hit renderer for.</param>
-        /// <param name="isForCurrentRuleset">Whether the hit renderer should assume the beatmap is for the current ruleset.</param>
         /// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
         /// <returns></returns>
-        public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset);
+        public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap);
 
-        public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null);
+        public abstract IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap);
 
-        public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null;
+        public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null;
+
+        public abstract DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null);
+
+        public virtual PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => null;
 
         public virtual HitObjectComposer CreateHitObjectComposer() => null;
 
@@ -114,7 +115,8 @@ namespace osu.Game.Rulesets
             Name = Description,
             ShortName = ShortName,
             InstantiationInfo = GetType().AssemblyQualifiedName,
-            ID = LegacyID
+            ID = LegacyID,
+            Available = true
         };
     }
 }
diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs
index 239f200e29..d5ab856697 100644
--- a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs
+++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Scoring.Legacy
             this.beatmaps = beatmaps;
         }
 
-        private Beatmap currentBeatmap;
+        private IBeatmap currentBeatmap;
         private Ruleset currentRuleset;
 
         public Score Parse(Stream stream)
diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs
index 0f115fff6b..5b8f5f0d0f 100644
--- a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs
+++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs
@@ -2,42 +2,28 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using System.Collections.Generic;
-using System.Linq;
 using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
 
 namespace osu.Game.Rulesets.Scoring
 {
     public abstract class PerformanceCalculator
-    {
-        public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
-    }
-
-    public abstract class PerformanceCalculator<TObject> : PerformanceCalculator
-        where TObject : HitObject
     {
         private readonly Dictionary<string, double> attributes = new Dictionary<string, double>();
         protected IDictionary<string, double> Attributes => attributes;
 
-        protected readonly Beatmap<TObject> Beatmap;
+        protected readonly IBeatmap Beatmap;
         protected readonly Score Score;
 
-        protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
+        protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
         {
             Score = score;
 
-            var converter = CreateBeatmapConverter();
-
-            foreach (var mod in score.Mods.OfType<IApplicableToBeatmapConverter<TObject>>())
-                mod.ApplyToBeatmapConverter(converter);
-
-            Beatmap = converter.Convert(beatmap);
+            Beatmap = beatmap;
 
             var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods);
             diffCalc.Calculate(attributes);
         }
 
-        protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
+        public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
     }
 }
diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs
index d1f1807937..e42e74c245 100644
--- a/osu.Game/Rulesets/UI/RulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/RulesetContainer.cs
@@ -190,11 +190,6 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         protected readonly WorkingBeatmap WorkingBeatmap;
 
-        /// <summary>
-        /// Whether the specified beatmap is assumed to be specific to the current ruleset.
-        /// </summary>
-        public readonly bool IsForCurrentRuleset;
-
         public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor<TObject>(this);
 
         protected override Container<Drawable> Content => content;
@@ -206,43 +201,18 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         /// <param name="ruleset">The ruleset being repesented.</param>
         /// <param name="workingBeatmap">The beatmap to create the hit renderer for.</param>
-        /// <param name="isForCurrentRuleset">Whether to assume the beatmap is for the current ruleset.</param>
-        protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap, bool isForCurrentRuleset)
+        protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap)
             : base(ruleset)
         {
             Debug.Assert(workingBeatmap != null, "RulesetContainer initialized with a null beatmap.");
 
             WorkingBeatmap = workingBeatmap;
-            IsForCurrentRuleset = isForCurrentRuleset;
             // ReSharper disable once PossibleNullReferenceException
             Mods = workingBeatmap.Mods.Value;
 
             RelativeSizeAxes = Axes.Both;
 
-            BeatmapConverter<TObject> converter = CreateBeatmapConverter();
-            BeatmapProcessor<TObject> processor = CreateBeatmapProcessor();
-
-            // Check if the beatmap can be converted
-            if (!converter.CanConvert(workingBeatmap.Beatmap))
-                throw new BeatmapInvalidForRulesetException($"{nameof(Beatmap)} can not be converted for the current ruleset (converter: {converter}).");
-
-            // Apply conversion adjustments before converting
-            foreach (var mod in Mods.OfType<IApplicableToBeatmapConverter<TObject>>())
-                mod.ApplyToBeatmapConverter(converter);
-
-            // Convert the beatmap
-            Beatmap = converter.Convert(workingBeatmap.Beatmap);
-
-            // Apply difficulty adjustments from mods before using Difficulty.
-            foreach (var mod in Mods.OfType<IApplicableToDifficulty>())
-                mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty);
-
-            // Post-process the beatmap
-            processor.PostProcess(Beatmap);
-
-            // Apply defaults
-            foreach (var h in Beatmap.HitObjects)
-                h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty);
+            Beatmap = (Beatmap<TObject>)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
 
             KeyBindingInputManager = CreateInputManager();
             KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
@@ -277,10 +247,6 @@ namespace osu.Game.Rulesets.UI
             if (mods == null)
                 return;
 
-            foreach (var mod in mods.OfType<IApplicableToHitObject<TObject>>())
-                foreach (var obj in Beatmap.HitObjects)
-                    mod.ApplyToHitObject(obj);
-
             foreach (var mod in mods.OfType<IApplicableToRulesetContainer<TObject>>())
                 mod.ApplyToRulesetContainer(this);
         }
@@ -324,13 +290,6 @@ namespace osu.Game.Rulesets.UI
             Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea;
         }
 
-        /// <summary>
-        /// Creates a processor to perform post-processing operations
-        /// on HitObjects in converted Beatmaps.
-        /// </summary>
-        /// <returns>The Beatmap processor.</returns>
-        protected virtual BeatmapProcessor<TObject> CreateBeatmapProcessor() => new BeatmapProcessor<TObject>();
-
         /// <summary>
         /// Computes the size of the <see cref="Playfield"/> in relative coordinate space after aspect adjustments.
         /// </summary>
@@ -344,12 +303,6 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default
 
-        /// <summary>
-        /// Creates a converter to convert Beatmap to a specific mode.
-        /// </summary>
-        /// <returns>The Beatmap converter.</returns>
-        protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
-
         /// <summary>
         /// Creates a DrawableHitObject from a HitObject.
         /// </summary>
@@ -377,9 +330,8 @@ namespace osu.Game.Rulesets.UI
         /// </summary>
         /// <param name="ruleset">The ruleset being repesented.</param>
         /// <param name="beatmap">The beatmap to create the hit renderer for.</param>
-        /// <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)
+        protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
     }
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index 1e7f561aba..6f86d20295 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -4,9 +4,7 @@
 using osu.Framework.Allocation;
 using osu.Framework.Configuration;
 using osu.Framework.Graphics;
-using osu.Framework.Graphics.Transforms;
 using osu.Framework.Input;
-using osu.Framework.MathUtils;
 using osu.Game.Rulesets.Objects.Drawables;
 using OpenTK.Input;
 
@@ -90,10 +88,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
                 switch (args.Key)
                 {
                     case Key.Minus:
-                        transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
+                        this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
                         break;
                     case Key.Plus:
-                        transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
+                        this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
                         break;
                 }
             }
@@ -101,27 +99,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
             return false;
         }
 
-        private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
-        {
-            this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
-        }
-
         protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction);
-
-        private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
-        {
-            private double valueAt(double time)
-            {
-                if (time < StartTime) return StartValue;
-                if (time >= EndTime) return EndValue;
-
-                return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
-            }
-
-            public override string TargetMember => "VisibleTimeRange.Value";
-
-            protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
-            protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
-        }
     }
 }
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs
index 79f9eaa9e9..efd901240a 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs
@@ -30,8 +30,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
         /// <returns></returns>
         protected readonly SortedList<MultiplierControlPoint> DefaultControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
 
-        protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
-            : base(ruleset, beatmap, isForCurrentRuleset)
+        protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+            : base(ruleset, beatmap)
         {
         }
 
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index adb749b492..e4eaee76fc 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit
         protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4");
 
         public override bool ShowOverlaysOnEnter => false;
+        public override bool AllowBeatmapRulesetChange => false;
 
         private Box bottomBackground;
         private Container screenContainer;
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index f2ea6d85a8..907ad81111 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -14,7 +14,7 @@ using osu.Game.Screens.Backgrounds;
 using osu.Game.Screens.Charts;
 using osu.Game.Screens.Direct;
 using osu.Game.Screens.Edit;
-using osu.Game.Screens.Multiplayer;
+using osu.Game.Screens.Multi.Screens;
 using osu.Game.Screens.Select;
 using osu.Game.Screens.Tournament;
 
diff --git a/osu.Game/Screens/Multiplayer/DrawableGameType.cs b/osu.Game/Screens/Multi/Components/DrawableGameType.cs
similarity index 96%
rename from osu.Game/Screens/Multiplayer/DrawableGameType.cs
rename to osu.Game/Screens/Multi/Components/DrawableGameType.cs
index 5790008f76..3406e179d4 100644
--- a/osu.Game/Screens/Multiplayer/DrawableGameType.cs
+++ b/osu.Game/Screens/Multi/Components/DrawableGameType.cs
@@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes;
 using osu.Game.Graphics;
 using osu.Game.Online.Multiplayer;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Components
 {
     public class DrawableGameType : CircularContainer, IHasTooltip
     {
diff --git a/osu.Game/Screens/Multiplayer/DrawableRoom.cs b/osu.Game/Screens/Multi/Components/DrawableRoom.cs
similarity index 99%
rename from osu.Game/Screens/Multiplayer/DrawableRoom.cs
rename to osu.Game/Screens/Multi/Components/DrawableRoom.cs
index d53100526f..040fbaf593 100644
--- a/osu.Game/Screens/Multiplayer/DrawableRoom.cs
+++ b/osu.Game/Screens/Multi/Components/DrawableRoom.cs
@@ -1,8 +1,6 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK;
-using OpenTK.Graphics;
 using osu.Framework.Allocation;
 using osu.Framework.Configuration;
 using osu.Framework.Extensions.Color4Extensions;
@@ -17,8 +15,10 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Users;
+using OpenTK;
+using OpenTK.Graphics;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Components
 {
     public class DrawableRoom : OsuClickableContainer
     {
diff --git a/osu.Game/Screens/Multiplayer/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
similarity index 98%
rename from osu.Game/Screens/Multiplayer/ModeTypeInfo.cs
rename to osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
index 08e96ba55d..e3aba685a7 100644
--- a/osu.Game/Screens/Multiplayer/ModeTypeInfo.cs
+++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
@@ -1,14 +1,14 @@
 // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
-using OpenTK;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Drawables;
 using osu.Game.Online.Multiplayer;
+using OpenTK;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Components
 {
     public class ModeTypeInfo : Container
     {
diff --git a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs b/osu.Game/Screens/Multi/Components/ParticipantInfo.cs
similarity index 99%
rename from osu.Game/Screens/Multiplayer/ParticipantInfo.cs
rename to osu.Game/Screens/Multi/Components/ParticipantInfo.cs
index ff1887fa17..ab404488f1 100644
--- a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs
+++ b/osu.Game/Screens/Multi/Components/ParticipantInfo.cs
@@ -3,7 +3,6 @@
 
 using System.Collections.Generic;
 using System.Linq;
-using OpenTK;
 using osu.Framework.Allocation;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
@@ -11,8 +10,9 @@ using osu.Framework.Graphics.Shapes;
 using osu.Game.Graphics;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Users;
+using OpenTK;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Components
 {
     public class ParticipantInfo : Container
     {
diff --git a/osu.Game/Screens/Multiplayer/RoomInspector.cs b/osu.Game/Screens/Multi/Components/RoomInspector.cs
similarity index 99%
rename from osu.Game/Screens/Multiplayer/RoomInspector.cs
rename to osu.Game/Screens/Multi/Components/RoomInspector.cs
index bfc4a44ed5..92910e8301 100644
--- a/osu.Game/Screens/Multiplayer/RoomInspector.cs
+++ b/osu.Game/Screens/Multi/Components/RoomInspector.cs
@@ -2,8 +2,6 @@
 // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
 
 using System.Linq;
-using OpenTK;
-using OpenTK.Graphics;
 using osu.Framework.Allocation;
 using osu.Framework.Configuration;
 using osu.Framework.Extensions.Color4Extensions;
@@ -20,8 +18,10 @@ using osu.Game.Graphics.Containers;
 using osu.Game.Graphics.Sprites;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Users;
+using OpenTK;
+using OpenTK.Graphics;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Components
 {
     public class RoomInspector : Container
     {
diff --git a/osu.Game/Screens/Multiplayer/Lobby.cs b/osu.Game/Screens/Multi/Screens/Lobby.cs
similarity index 90%
rename from osu.Game/Screens/Multiplayer/Lobby.cs
rename to osu.Game/Screens/Multi/Screens/Lobby.cs
index 65fa5fbb16..dcda40e0d7 100644
--- a/osu.Game/Screens/Multiplayer/Lobby.cs
+++ b/osu.Game/Screens/Multi/Screens/Lobby.cs
@@ -4,7 +4,7 @@
 using System;
 using System.Collections.Generic;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Screens
 {
     public class Lobby : ScreenWhiteBox
     {
diff --git a/osu.Game/Screens/Multiplayer/Match.cs b/osu.Game/Screens/Multi/Screens/Match.cs
similarity index 96%
rename from osu.Game/Screens/Multiplayer/Match.cs
rename to osu.Game/Screens/Multi/Screens/Match.cs
index 5402e70ea5..4ba7fe9f6a 100644
--- a/osu.Game/Screens/Multiplayer/Match.cs
+++ b/osu.Game/Screens/Multi/Screens/Match.cs
@@ -3,14 +3,14 @@
 
 using System;
 using System.Collections.Generic;
+using osu.Framework.Graphics;
 using osu.Framework.Screens;
 using osu.Game.Screens.Backgrounds;
 using osu.Game.Screens.Play;
-using OpenTK.Graphics;
 using osu.Game.Screens.Select;
-using osu.Framework.Graphics;
+using OpenTK.Graphics;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Screens
 {
     public class Match : ScreenWhiteBox
     {
diff --git a/osu.Game/Screens/Multiplayer/MatchCreate.cs b/osu.Game/Screens/Multi/Screens/MatchCreate.cs
similarity index 91%
rename from osu.Game/Screens/Multiplayer/MatchCreate.cs
rename to osu.Game/Screens/Multi/Screens/MatchCreate.cs
index ca6b814cb9..6b4e26d5e5 100644
--- a/osu.Game/Screens/Multiplayer/MatchCreate.cs
+++ b/osu.Game/Screens/Multi/Screens/MatchCreate.cs
@@ -4,7 +4,7 @@
 using System;
 using System.Collections.Generic;
 
-namespace osu.Game.Screens.Multiplayer
+namespace osu.Game.Screens.Multi.Screens
 {
     public class MatchCreate : ScreenWhiteBox
     {
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 83958b2912..f397d0c3d4 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -93,7 +93,7 @@ namespace osu.Game.Screens.Play
             mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
             userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
 
-            Beatmap beatmap;
+            IBeatmap beatmap;
 
             try
             {
@@ -107,7 +107,7 @@ namespace osu.Game.Screens.Play
 
                 try
                 {
-                    RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working, ruleset.ID == beatmap.BeatmapInfo.Ruleset.ID);
+                    RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working);
                 }
                 catch (BeatmapInvalidForRulesetException)
                 {
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play
                     // let's try again forcing the beatmap's ruleset.
                     ruleset = beatmap.BeatmapInfo.Ruleset;
                     rulesetInstance = ruleset.CreateInstance();
-                    RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap, true);
+                    RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap);
                 }
 
                 if (!RulesetContainer.Objects.Any())
diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
index f005261ffa..236b1310e1 100644
--- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs
+++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs
@@ -4,9 +4,11 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using JetBrains.Annotations;
 using OpenTK;
 using OpenTK.Graphics;
 using osu.Framework.Allocation;
+using osu.Framework.Configuration;
 using osu.Framework.Extensions.Color4Extensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Colour;
@@ -21,6 +23,8 @@ using osu.Game.Rulesets.Objects.Types;
 using osu.Framework.Graphics.Shapes;
 using osu.Framework.Graphics.Cursor;
 using osu.Framework.Localisation;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.UI;
 
 namespace osu.Game.Screens.Select
 {
@@ -28,6 +32,8 @@ namespace osu.Game.Screens.Select
     {
         private static readonly Vector2 wedged_container_shear = new Vector2(0.15f, 0);
 
+        private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
+
         protected BufferedWedgeInfo Info;
 
         public BeatmapInfoWedge()
@@ -46,6 +52,14 @@ namespace osu.Game.Screens.Select
             };
         }
 
+        [BackgroundDependencyLoader(true)]
+        private void load([CanBeNull] OsuGame osuGame)
+        {
+            if (osuGame != null)
+                ruleset.BindTo(osuGame.Ruleset);
+            ruleset.ValueChanged += updateRuleset;
+        }
+
         protected override bool BlockPassThroughMouse => false;
 
         protected override void PopIn()
@@ -62,9 +76,19 @@ namespace osu.Game.Screens.Select
             this.FadeOut(500, Easing.In);
         }
 
+        private WorkingBeatmap beatmap;
+
         public void UpdateBeatmap(WorkingBeatmap beatmap)
         {
-            LoadComponentAsync(new BufferedWedgeInfo(beatmap)
+            this.beatmap = beatmap;
+            loadBeatmap();
+        }
+
+        private void updateRuleset(RulesetInfo ruleset) => loadBeatmap();
+
+        private void loadBeatmap()
+        {
+            LoadComponentAsync(new BufferedWedgeInfo(beatmap, ruleset.Value)
             {
                 Shear = -Shear,
                 Depth = Info?.Depth + 1 ?? 0,
@@ -90,9 +114,13 @@ namespace osu.Game.Screens.Select
             private UnicodeBindableString titleBinding;
             private UnicodeBindableString artistBinding;
 
-            public BufferedWedgeInfo(WorkingBeatmap working)
+            private readonly RulesetInfo ruleset;
+
+            public BufferedWedgeInfo(WorkingBeatmap working, RulesetInfo userRuleset)
             {
                 this.working = working;
+
+                ruleset = userRuleset ?? working.BeatmapInfo.Ruleset;
             }
 
             [BackgroundDependencyLoader]
@@ -211,11 +239,10 @@ namespace osu.Game.Screens.Select
             private InfoLabel[] getInfoLabels()
             {
                 var beatmap = working.Beatmap;
-                var info = working.BeatmapInfo;
 
                 List<InfoLabel> labels = new List<InfoLabel>();
 
-                if (beatmap?.HitObjects?.Count > 0)
+                if (beatmap?.HitObjects?.Any() == true)
                 {
                     HitObject lastObject = beatmap.HitObjects.LastOrDefault();
                     double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0;
@@ -224,7 +251,7 @@ namespace osu.Game.Screens.Select
                     {
                         Name = "Length",
                         Icon = FontAwesome.fa_clock_o,
-                        Content = beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"),
+                        Content = TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"),
                     }));
 
                     labels.Add(new InfoLabel(new BeatmapStatistic
@@ -234,14 +261,26 @@ namespace osu.Game.Screens.Select
                         Content = getBPMRange(beatmap),
                     }));
 
-                    //get statistics from the current ruleset.
-                    labels.AddRange(info.Ruleset.CreateInstance().GetBeatmapStatistics(working).Select(s => new InfoLabel(s)));
+                    IBeatmap playableBeatmap;
+
+                    try
+                    {
+                        // Try to get the beatmap with the user's ruleset
+                        playableBeatmap = working.GetPlayableBeatmap(ruleset);
+                    }
+                    catch (BeatmapInvalidForRulesetException)
+                    {
+                        // Can't be converted to the user's ruleset, so use the beatmap's own ruleset
+                        playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset);
+                    }
+
+                    labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)));
                 }
 
                 return labels.ToArray();
             }
 
-            private string getBPMRange(Beatmap beatmap)
+            private string getBPMRange(IBeatmap beatmap)
             {
                 double bpmMax = beatmap.ControlPointInfo.BPMMaximum;
                 double bpmMin = beatmap.ControlPointInfo.BPMMinimum;
diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
index 2850de8ba5..2de38d49a9 100644
--- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
+++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs
@@ -10,12 +10,14 @@ using NUnit.Framework;
 using osu.Framework.Extensions.IEnumerableExtensions;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.Formats;
+using osu.Game.Rulesets;
 using osu.Game.Rulesets.Objects;
 
 namespace osu.Game.Tests.Beatmaps
 {
     [TestFixture]
-    public abstract class BeatmapConversionTest<TConvertValue>
+    public abstract class BeatmapConversionTest<TRuleset, TConvertValue>
+        where TRuleset : Ruleset, new()
         where TConvertValue : IEquatable<TConvertValue>
     {
         private const string resource_namespace = "Testing.Beatmaps";
@@ -79,6 +81,9 @@ namespace osu.Game.Tests.Beatmaps
         {
             var beatmap = getBeatmap(name);
 
+            var rulesetInstance = new TRuleset();
+            beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo();
+
             var result = new ConvertResult();
 
             var converter = CreateConverter(beatmap);
@@ -92,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
                 result.Mappings.Add(mapping);
             };
 
-            converter.Convert(beatmap);
+            converter.Convert();
 
             return result;
         }
@@ -107,7 +112,7 @@ namespace osu.Game.Tests.Beatmaps
             }
         }
 
-        private Beatmap getBeatmap(string name)
+        private IBeatmap getBeatmap(string name)
         {
             using (var resStream = openResource($"{resource_namespace}.{name}.osu"))
             using (var stream = new StreamReader(resStream))
@@ -125,7 +130,7 @@ namespace osu.Game.Tests.Beatmaps
         }
 
         protected abstract IEnumerable<TConvertValue> CreateConvertValue(HitObject hitObject);
-        protected abstract IBeatmapConverter CreateConverter(Beatmap beatmap);
+        protected abstract IBeatmapConverter CreateConverter(IBeatmap beatmap);
 
         private class ConvertMapping
         {
diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
index 09a3a7af8c..6bad08baaa 100644
--- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs
@@ -12,8 +12,14 @@ namespace osu.Game.Tests.Beatmaps
     public class TestBeatmap : Beatmap
     {
         public TestBeatmap(RulesetInfo ruleset)
-        : base(createTestBeatmap())
         {
+            var baseBeatmap = createTestBeatmap();
+
+            BeatmapInfo = baseBeatmap.BeatmapInfo;
+            ControlPointInfo = baseBeatmap.ControlPointInfo;
+            Breaks = baseBeatmap.Breaks;
+            HitObjects = baseBeatmap.HitObjects;
+
             BeatmapInfo.Ruleset = ruleset;
         }
 
diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
index e24fbab3ac..37693c99e8 100644
--- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
+++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs
@@ -17,14 +17,14 @@ namespace osu.Game.Tests.Beatmaps
         {
         }
 
-        public TestWorkingBeatmap(Beatmap beatmap)
+        public TestWorkingBeatmap(IBeatmap beatmap)
             : base(beatmap.BeatmapInfo)
         {
             this.beatmap = beatmap;
         }
 
-        private readonly Beatmap beatmap;
-        protected override Beatmap GetBeatmap() => beatmap;
+        private readonly IBeatmap beatmap;
+        protected override IBeatmap GetBeatmap() => beatmap;
         protected override Texture GetBackground() => null;
 
         protected override Track GetTrack()
diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
index 29132258c2..51460ecb6d 100644
--- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
+++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs
@@ -259,9 +259,9 @@ namespace osu.Game.Tests.Visual
                 private readonly OsuSpriteText text;
 
                 private readonly Score score;
-                private readonly Beatmap beatmap;
+                private readonly IBeatmap beatmap;
 
-                public PerformanceDisplay(Score score, Beatmap beatmap)
+                public PerformanceDisplay(Score score, IBeatmap beatmap)
                 {
                     this.score = score;
                     this.beatmap = beatmap;
diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs
index 5ed43b2814..bda438d906 100644
--- a/osu.Game/Tests/Visual/TestCasePlayer.cs
+++ b/osu.Game/Tests/Visual/TestCasePlayer.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual
             }
         }
 
-        protected virtual Beatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
+        protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
 
         private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance());